Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
android:taskAffinity=".CountlyPushActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false"/>

<provider
android:name="ly.count.android.sdk.CountlyInitProvider"
android:authorities="${applicationId}.countlyInitProvider"
android:exported="false"/>
</application>

<permission
Expand Down
24 changes: 23 additions & 1 deletion sdk/src/main/java/ly/count/android/sdk/Countly.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -811,6 +811,28 @@ public void onLowMemory() {
config.deviceInfo.inForeground();
}

// Seed modules with the current activity if the app is already in the foreground.
// This handles frameworks (Flutter, React Native) and single-activity apps where
// the host activity is already started before the SDK registers its lifecycle callbacks.
// Priority: explicit initialActivity from config, then ContentProvider-tracked activity.
Activity seedActivity = null;
if (config.initialActivity != null && !config.initialActivity.isFinishing()) {
seedActivity = config.initialActivity;
config.initialActivity = null;
} else {
Activity holderActivity = CountlyActivityHolder.getInstance().getActivity();
if (holderActivity != null && !holderActivity.isFinishing()) {
seedActivity = holderActivity;
}
}

if (seedActivity != null) {
L.d("[Countly] Seeding modules with initial activity: [" + seedActivity.getClass().getSimpleName() + "]");
for (ModuleBase module : modules) {
module.onInitialActivitySeeded(seedActivity);
}
}

L.i("[Init] About to call module 'initFinished'");

for (ModuleBase module : modules) {
Expand Down
44 changes: 44 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/CountlyActivityHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package ly.count.android.sdk;

import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;

/**
* Singleton that holds a WeakReference to the current foreground Activity.
* Populated by {@link CountlyInitProvider} via ActivityLifecycleCallbacks
* registered before Application.onCreate(), ensuring the first Activity is never missed.
*/
class CountlyActivityHolder {
private static final CountlyActivityHolder instance = new CountlyActivityHolder();
private @Nullable WeakReference<Activity> 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;
}
}
17 changes: 17 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/CountlyConfig.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -175,6 +176,8 @@ public class CountlyConfig {

protected Application application = null;

protected Activity initialActivity = null;

boolean disableLocation = false;

String locationCountyCode = null;
Expand Down Expand Up @@ -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
*
Expand Down
97 changes: 97 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/CountlyInitProvider.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
8 changes: 8 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/ModuleBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
9 changes: 9 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/ModuleContent.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading