From 57007ab37bd15c25d94918f19d19d533361c6d0a Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 7 Apr 2026 14:57:02 +0300 Subject: [PATCH 1/2] feat: content provider --- CHANGELOG.md | 4 + sdk/src/main/AndroidManifest.xml | 5 + .../java/ly/count/android/sdk/Countly.java | 22 +++++ .../android/sdk/CountlyActivityHolder.java | 44 +++++++++ .../ly/count/android/sdk/CountlyConfig.java | 17 ++++ .../android/sdk/CountlyInitProvider.java | 97 +++++++++++++++++++ .../java/ly/count/android/sdk/ModuleBase.java | 8 ++ .../ly/count/android/sdk/ModuleContent.java | 9 ++ .../ly/count/android/sdk/ModuleFeedback.java | 6 ++ 9 files changed, 212 insertions(+) create mode 100644 sdk/src/main/java/ly/count/android/sdk/CountlyActivityHolder.java create mode 100644 sdk/src/main/java/ly/count/android/sdk/CountlyInitProvider.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c1733acfb..4309d5368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 26.1.2 +* Added `CountlyInitProvider` ContentProvider to register activity lifecycle callbacks before `Application.onCreate()`. This ensures the SDK captures the current activity in single-activity frameworks (Flutter, React Native) and apps with deferred initialization. +* Added `CountlyConfig.setInitialActivity(Activity)` as an explicit way for wrapper SDKs to provide the host activity during initialization. + ## 26.1.1 * Added Content feature method `previewContent(String contentId)` (Experimental!). * Improved content display and refresh mechanics. diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml index c30a4ee07..e80a441e3 100644 --- a/sdk/src/main/AndroidManifest.xml +++ b/sdk/src/main/AndroidManifest.xml @@ -9,6 +9,11 @@ android:taskAffinity=".CountlyPushActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"/> + + currentActivity; + + private CountlyActivityHolder() { + } + + static CountlyActivityHolder getInstance() { + return instance; + } + + @Nullable Activity getActivity() { + if (currentActivity != null) { + return currentActivity.get(); + } + return null; + } + + void setActivity(@NonNull Activity activity) { + if (currentActivity != null && currentActivity.get() == activity) { + return; + } + currentActivity = new WeakReference<>(activity); + } + + void clearActivity(@NonNull Activity activity) { + if (currentActivity != null && currentActivity.get() != activity) { + return; + } + currentActivity = null; + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java index 3b2131826..b19598235 100644 --- a/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java @@ -1,5 +1,6 @@ package ly.count.android.sdk; +import android.app.Activity; import android.app.Application; import android.content.Context; import java.util.ArrayList; @@ -175,6 +176,8 @@ public class CountlyConfig { protected Application application = null; + protected Activity initialActivity = null; + boolean disableLocation = false; String locationCountyCode = null; @@ -845,6 +848,20 @@ public synchronized CountlyConfig setApplication(Application application) { return this; } + /** + * Set the initial activity reference for SDK initialization. + * This is needed for frameworks like Flutter and React Native where the host activity + * is already started before the SDK registers its lifecycle callbacks. + * Setting this ensures that content overlays and feedback widgets can display correctly. + * + * @param activity the current foreground activity + * @return Returns the same config object for convenient linking + */ + public synchronized CountlyConfig setInitialActivity(Activity activity) { + this.initialActivity = activity; + return this; + } + /** * Enable the recording of the app start time * diff --git a/sdk/src/main/java/ly/count/android/sdk/CountlyInitProvider.java b/sdk/src/main/java/ly/count/android/sdk/CountlyInitProvider.java new file mode 100644 index 000000000..099d1b632 --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/CountlyInitProvider.java @@ -0,0 +1,97 @@ +package ly.count.android.sdk; + +import android.app.Activity; +import android.app.Application; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * ContentProvider that registers ActivityLifecycleCallbacks before Application.onCreate(). + * This ensures that the SDK captures the first Activity reference even when Countly.init() + * is called after the Activity has already started (e.g., in Flutter, React Native, or + * single-activity apps with deferred initialization). + * + * The captured Activity is stored in {@link CountlyActivityHolder} and used during + * SDK initialization to seed modules that need an Activity reference. + * + * This provider performs no actual content operations. + */ +public class CountlyInitProvider extends ContentProvider { + @Override + public boolean onCreate() { + Context context = getContext(); + if (context == null) { + return false; + } + + Context appContext = context.getApplicationContext(); + if (appContext instanceof Application) { + ((Application) appContext).registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + CountlyActivityHolder.getInstance().setActivity(activity); + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + CountlyActivityHolder.getInstance().setActivity(activity); + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + CountlyActivityHolder.getInstance().setActivity(activity); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + CountlyActivityHolder.getInstance().clearActivity(activity); + } + }); + } + + return false; + } + + @Nullable @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { + return null; + } + + @Nullable @Override + public String getType(@NonNull Uri uri) { + return null; + } + + @Nullable @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } +} diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java index 518b8afde..4945d2840 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleBase.java @@ -59,6 +59,14 @@ void onConfigurationChanged(Configuration newConfig) { void onActivityStarted(Activity activity, int updatedActivityCount) { } + /** + * Called during init when the app is already in the foreground and an initial activity + * was provided via CountlyConfig.setInitialActivity(). This only sets the activity + * reference without triggering counters, sessions, or view tracking. + */ + void onInitialActivitySeeded(@NonNull Activity activity) { + } + /** * Called manually by a countly call from the developer */ diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java index e6f18f610..d9b7f730b 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -66,6 +66,15 @@ void initFinished(@NotNull CountlyConfig config) { } } + @Override + void onInitialActivitySeeded(@NonNull Activity activity) { + L.d("[ModuleContent] onInitialActivitySeeded, activity: [" + activity.getClass().getSimpleName() + "]"); + currentActivity = activity; + if (UtilsDevice.cutout == null) { + UtilsDevice.getCutout(activity); + } + } + @Override void onActivityStarted(Activity activity, int updatedActivityCount) { if (activity == null) { diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java index 6c8ee07d2..0b3d40d8a 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java @@ -55,6 +55,12 @@ public static class CountlyFeedbackWidget implements Serializable { feedbackInterface = new Feedback(); } + @Override + void onInitialActivitySeeded(@NonNull Activity activity) { + L.d("[ModuleFeedback] onInitialActivitySeeded, activity: [" + activity.getClass().getSimpleName() + "]"); + currentActivity = activity; + } + @Override void onActivityStarted(Activity activity, int updatedActivityCount) { if (activity == null) { From 6aad6c527ec80a549acd26f5df2646b02b4540cc Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 7 Apr 2026 14:58:08 +0300 Subject: [PATCH 2/2] feat: 26.1.2-rc1 --- gradle.properties | 2 +- sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java | 2 +- sdk/src/main/java/ly/count/android/sdk/Countly.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index c1475ef77..e9480e0c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ org.gradle.configureondemand=true android.useAndroidX=true android.enableJetifier=true # RELEASE FIELD SECTION -VERSION_NAME=26.1.1 +VERSION_NAME=26.1.2-RC1 GROUP=ly.count.android POM_URL=https://github.com/Countly/countly-sdk-android POM_SCM_URL=https://github.com/Countly/countly-sdk-android diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java index 1e7ad7de4..d075efcdb 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java @@ -44,7 +44,7 @@ public class TestUtils { public final static String commonAppKey = "appkey"; public final static String commonDeviceId = "1234"; public final static String SDK_NAME = "java-native-android"; - public final static String SDK_VERSION = "26.1.1"; + public final static String SDK_VERSION = "26.1.2-RC1"; public static final int MAX_THREAD_COUNT_PER_STACK_TRACE = 50; public static class Activity2 extends Activity { diff --git a/sdk/src/main/java/ly/count/android/sdk/Countly.java b/sdk/src/main/java/ly/count/android/sdk/Countly.java index cb0f3e70b..b909d418d 100644 --- a/sdk/src/main/java/ly/count/android/sdk/Countly.java +++ b/sdk/src/main/java/ly/count/android/sdk/Countly.java @@ -47,7 +47,7 @@ of this software and associated documentation files (the "Software"), to deal */ public class Countly { - private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "26.1.1"; + private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "26.1.2-RC1"; /** * Used as request meta data on every request */