Skip to content

Commit a18f363

Browse files
Merge pull request #550 from Countly/staging
Staging
2 parents 9880d38 + 96beb8f commit a18f363

11 files changed

Lines changed: 215 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 26.1.2
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.
3+
* Added `CountlyConfig.setInitialActivity(Activity)` as an explicit way for wrapper SDKs to provide the host activity during initialization.
4+
15
## 26.1.1
26
* Added Content feature method `previewContent(String contentId)` (Experimental!).
37
* Improved content display and refresh mechanics.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ org.gradle.configureondemand=true
2222
android.useAndroidX=true
2323
android.enableJetifier=true
2424
# RELEASE FIELD SECTION
25-
VERSION_NAME=26.1.1
25+
VERSION_NAME=26.1.2-RC1
2626
GROUP=ly.count.android
2727
POM_URL=https://github.com/Countly/countly-sdk-android
2828
POM_SCM_URL=https://github.com/Countly/countly-sdk-android

sdk/src/androidTest/java/ly/count/android/sdk/TestUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class TestUtils {
4444
public final static String commonAppKey = "appkey";
4545
public final static String commonDeviceId = "1234";
4646
public final static String SDK_NAME = "java-native-android";
47-
public final static String SDK_VERSION = "26.1.1";
47+
public final static String SDK_VERSION = "26.1.2-RC1";
4848
public static final int MAX_THREAD_COUNT_PER_STACK_TRACE = 50;
4949

5050
public static class Activity2 extends Activity {

sdk/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
android:taskAffinity=".CountlyPushActivity"
1010
android:theme="@android:style/Theme.Translucent.NoTitleBar"
1111
android:exported="false"/>
12+
13+
<provider
14+
android:name="ly.count.android.sdk.CountlyInitProvider"
15+
android:authorities="${applicationId}.countlyInitProvider"
16+
android:exported="false"/>
1217
</application>
1318

1419
<permission

sdk/src/main/java/ly/count/android/sdk/Countly.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ of this software and associated documentation files (the "Software"), to deal
4747
*/
4848
public class Countly {
4949

50-
private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "26.1.1";
50+
private final String DEFAULT_COUNTLY_SDK_VERSION_STRING = "26.1.2-RC1";
5151
/**
5252
* Used as request meta data on every request
5353
*/
@@ -811,6 +811,28 @@ public void onLowMemory() {
811811
config.deviceInfo.inForeground();
812812
}
813813

814+
// Seed modules with the current activity if the app is already in the foreground.
815+
// This handles frameworks (Flutter, React Native) and single-activity apps where
816+
// the host activity is already started before the SDK registers its lifecycle callbacks.
817+
// Priority: explicit initialActivity from config, then ContentProvider-tracked activity.
818+
Activity seedActivity = null;
819+
if (config.initialActivity != null && !config.initialActivity.isFinishing()) {
820+
seedActivity = config.initialActivity;
821+
config.initialActivity = null;
822+
} else {
823+
Activity holderActivity = CountlyActivityHolder.getInstance().getActivity();
824+
if (holderActivity != null && !holderActivity.isFinishing()) {
825+
seedActivity = holderActivity;
826+
}
827+
}
828+
829+
if (seedActivity != null) {
830+
L.d("[Countly] Seeding modules with initial activity: [" + seedActivity.getClass().getSimpleName() + "]");
831+
for (ModuleBase module : modules) {
832+
module.onInitialActivitySeeded(seedActivity);
833+
}
834+
}
835+
814836
L.i("[Init] About to call module 'initFinished'");
815837

816838
for (ModuleBase module : modules) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ly.count.android.sdk;
2+
3+
import android.app.Activity;
4+
import androidx.annotation.NonNull;
5+
import androidx.annotation.Nullable;
6+
import java.lang.ref.WeakReference;
7+
8+
/**
9+
* Singleton that holds a WeakReference to the current foreground Activity.
10+
* Populated by {@link CountlyInitProvider} via ActivityLifecycleCallbacks
11+
* registered before Application.onCreate(), ensuring the first Activity is never missed.
12+
*/
13+
class CountlyActivityHolder {
14+
private static final CountlyActivityHolder instance = new CountlyActivityHolder();
15+
private @Nullable WeakReference<Activity> currentActivity;
16+
17+
private CountlyActivityHolder() {
18+
}
19+
20+
static CountlyActivityHolder getInstance() {
21+
return instance;
22+
}
23+
24+
@Nullable Activity getActivity() {
25+
if (currentActivity != null) {
26+
return currentActivity.get();
27+
}
28+
return null;
29+
}
30+
31+
void setActivity(@NonNull Activity activity) {
32+
if (currentActivity != null && currentActivity.get() == activity) {
33+
return;
34+
}
35+
currentActivity = new WeakReference<>(activity);
36+
}
37+
38+
void clearActivity(@NonNull Activity activity) {
39+
if (currentActivity != null && currentActivity.get() != activity) {
40+
return;
41+
}
42+
currentActivity = null;
43+
}
44+
}

sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ly.count.android.sdk;
22

3+
import android.app.Activity;
34
import android.app.Application;
45
import android.content.Context;
56
import java.util.ArrayList;
@@ -175,6 +176,8 @@ public class CountlyConfig {
175176

176177
protected Application application = null;
177178

179+
protected Activity initialActivity = null;
180+
178181
boolean disableLocation = false;
179182

180183
String locationCountyCode = null;
@@ -858,6 +861,20 @@ public synchronized CountlyConfig setApplication(Application application) {
858861
return this;
859862
}
860863

864+
/**
865+
* Set the initial activity reference for SDK initialization.
866+
* This is needed for frameworks like Flutter and React Native where the host activity
867+
* is already started before the SDK registers its lifecycle callbacks.
868+
* Setting this ensures that content overlays and feedback widgets can display correctly.
869+
*
870+
* @param activity the current foreground activity
871+
* @return Returns the same config object for convenient linking
872+
*/
873+
public synchronized CountlyConfig setInitialActivity(Activity activity) {
874+
this.initialActivity = activity;
875+
return this;
876+
}
877+
861878
/**
862879
* Enable the recording of the app start time
863880
*
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package ly.count.android.sdk;
2+
3+
import android.app.Activity;
4+
import android.app.Application;
5+
import android.content.ContentProvider;
6+
import android.content.ContentValues;
7+
import android.content.Context;
8+
import android.database.Cursor;
9+
import android.net.Uri;
10+
import android.os.Bundle;
11+
import androidx.annotation.NonNull;
12+
import androidx.annotation.Nullable;
13+
14+
/**
15+
* ContentProvider that registers ActivityLifecycleCallbacks before Application.onCreate().
16+
* This ensures that the SDK captures the first Activity reference even when Countly.init()
17+
* is called after the Activity has already started (e.g., in Flutter, React Native, or
18+
* single-activity apps with deferred initialization).
19+
*
20+
* The captured Activity is stored in {@link CountlyActivityHolder} and used during
21+
* SDK initialization to seed modules that need an Activity reference.
22+
*
23+
* This provider performs no actual content operations.
24+
*/
25+
public class CountlyInitProvider extends ContentProvider {
26+
@Override
27+
public boolean onCreate() {
28+
Context context = getContext();
29+
if (context == null) {
30+
return false;
31+
}
32+
33+
Context appContext = context.getApplicationContext();
34+
if (appContext instanceof Application) {
35+
((Application) appContext).registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
36+
@Override
37+
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
38+
CountlyActivityHolder.getInstance().setActivity(activity);
39+
}
40+
41+
@Override
42+
public void onActivityStarted(@NonNull Activity activity) {
43+
CountlyActivityHolder.getInstance().setActivity(activity);
44+
}
45+
46+
@Override
47+
public void onActivityResumed(@NonNull Activity activity) {
48+
CountlyActivityHolder.getInstance().setActivity(activity);
49+
}
50+
51+
@Override
52+
public void onActivityPaused(@NonNull Activity activity) {
53+
}
54+
55+
@Override
56+
public void onActivityStopped(@NonNull Activity activity) {
57+
}
58+
59+
@Override
60+
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
61+
}
62+
63+
@Override
64+
public void onActivityDestroyed(@NonNull Activity activity) {
65+
CountlyActivityHolder.getInstance().clearActivity(activity);
66+
}
67+
});
68+
}
69+
70+
return false;
71+
}
72+
73+
@Nullable @Override
74+
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
75+
return null;
76+
}
77+
78+
@Nullable @Override
79+
public String getType(@NonNull Uri uri) {
80+
return null;
81+
}
82+
83+
@Nullable @Override
84+
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
85+
return null;
86+
}
87+
88+
@Override
89+
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
90+
return 0;
91+
}
92+
93+
@Override
94+
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
95+
return 0;
96+
}
97+
}

sdk/src/main/java/ly/count/android/sdk/ModuleBase.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ void onConfigurationChanged(Configuration newConfig) {
5959
void onActivityStarted(Activity activity, int updatedActivityCount) {
6060
}
6161

62+
/**
63+
* Called during init when the app is already in the foreground and an initial activity
64+
* was provided via CountlyConfig.setInitialActivity(). This only sets the activity
65+
* reference without triggering counters, sessions, or view tracking.
66+
*/
67+
void onInitialActivitySeeded(@NonNull Activity activity) {
68+
}
69+
6270
/**
6371
* Called manually by a countly call from the developer
6472
*/

sdk/src/main/java/ly/count/android/sdk/ModuleContent.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ void initFinished(@NotNull CountlyConfig config) {
6666
}
6767
}
6868

69+
@Override
70+
void onInitialActivitySeeded(@NonNull Activity activity) {
71+
L.d("[ModuleContent] onInitialActivitySeeded, activity: [" + activity.getClass().getSimpleName() + "]");
72+
currentActivity = activity;
73+
if (UtilsDevice.cutout == null) {
74+
UtilsDevice.getCutout(activity);
75+
}
76+
}
77+
6978
@Override
7079
void onActivityStarted(Activity activity, int updatedActivityCount) {
7180
if (activity == null) {

0 commit comments

Comments
 (0)