Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
db370fc
feat: update 24.7.0 changelog
arifBurakDemiray Apr 13, 2026
7e1f7f2
feat: configuration cache compatible plugin
arifBurakDemiray Apr 14, 2026
3aed04d
feat: configuration cache compatible plugin: changelog
arifBurakDemiray Apr 14, 2026
7c6c86b
feat: user props call trigger eq flush: changelog
arifBurakDemiray Apr 28, 2026
7939fa6
feat: user props call trigger eq flush: impl
arifBurakDemiray Apr 28, 2026
2bbb227
feat: tests
arifBurakDemiray Apr 28, 2026
4ca6af1
feat: tests
arifBurakDemiray Apr 28, 2026
99145be
Merge pull request #555 from Countly/fix_up_events_save
arifBurakDemiray Apr 29, 2026
18e4bd4
Merge pull request #552 from Countly/update_2470_changelog
arifBurakDemiray Apr 29, 2026
3da18e4
feat: activity memory leak for contents: changelog
arifBurakDemiray Apr 30, 2026
7ec0396
feat: activity memory leak for contents: test
arifBurakDemiray Apr 30, 2026
c1a52b2
feat: activity destroyed callback accross modules
arifBurakDemiray Apr 30, 2026
40309aa
feat: better activity transition
arifBurakDemiray Apr 30, 2026
bb26fd5
feat: strict mode fix context usage violations
arifBurakDemiray May 1, 2026
f6d35d5
Merge pull request #559 from Countly/fix556/memory_leak
arifBurakDemiray May 1, 2026
751a3b7
merge: fix452
arifBurakDemiray May 1, 2026
b1768d6
fix: direct key events to background view for contents
arifBurakDemiray May 1, 2026
9b9abf2
merge: direct inputs PR
arifBurakDemiray May 1, 2026
07c3a9d
fix: awaiter issue
arifBurakDemiray May 7, 2026
f8af45f
mixed: content test runner
arifBurakDemiray May 7, 2026
da2e180
feat: run on every content pr
arifBurakDemiray May 7, 2026
b7acf0e
feat: update emulator to 30
arifBurakDemiray May 7, 2026
0c19a0e
fix: manifest export
arifBurakDemiray May 7, 2026
8e9a941
fix: emulator version fix
arifBurakDemiray May 7, 2026
0e8e637
fix: uidump
arifBurakDemiray May 7, 2026
ede3f53
fix: chrome issue on runner
arifBurakDemiray May 7, 2026
792fb42
fix: script
arifBurakDemiray May 7, 2026
5fa0e45
fix: script
arifBurakDemiray May 7, 2026
ec23cab
fix: blind tap
arifBurakDemiray May 8, 2026
4b041b7
Merge branch 'staging' into uploadp_cc
arifBurakDemiray May 11, 2026
9efbb78
Merge pull request #553 from Countly/uploadp_cc
arifBurakDemiray May 11, 2026
392a298
Merge pull request #557 from Countly/fix556/memory_leak
arifBurakDemiray May 11, 2026
c1ab0a2
Merge branch 'staging' into fix_background_inputs_flow_contents
arifBurakDemiray May 11, 2026
00d34fb
Merge pull request #561 from Countly/fix_background_inputs_flow_contents
arifBurakDemiray May 11, 2026
7fbc55d
Merge branch 'staging' into fix452/strict_mode_content
arifBurakDemiray May 11, 2026
118010c
Merge pull request #558 from Countly/fix452/strict_mode_content
arifBurakDemiray May 11, 2026
33778ad
Merge branch 'staging' into staging01052026
arifBurakDemiray May 11, 2026
88caec1
fix: post merge
arifBurakDemiray May 11, 2026
fd8c488
feat: strict mode configurator kt
arifBurakDemiray May 11, 2026
34dd8b9
fix: tests
arifBurakDemiray May 11, 2026
ce6f769
Merge pull request #562 from Countly/staging01052026
arifBurakDemiray May 11, 2026
6fdf2a4
feat: 26.1.3
arifBurakDemiray May 12, 2026
7574a09
Merge pull request #564 from Countly/26_1_3
arifBurakDemiray May 12, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ upload-plugin/build/pluginUnderTestMetadata/plugin-under-test-metadata.propertie
upload-plugin/build/
app-native/.cxx/
.vscode/

# Content/feedback UI test runner artifacts (videos, logcat, verdicts)
.github/scripts/test_output/
.github/scripts/__pycache__/
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 26.1.3
* Added gradle configuration cache support to upload symbols plugin.
* Improved user properties auto-save conditions to flush event queue with every user property call.

* Mitigated `IncorrectContextUseViolation` StrictMode warnings from non-UI context use in display metrics and content overlay construction.
* Mitigated an issue where content overlays and feedback widgets blocked keyboard input on the host activity.
* Mitigated a memory retention issue where content overlays and feedback widgets were briefly held after closing.
* Mitigated a memory leak where the content overlay retained its initial host activity across activity transitions.
* Mitigated a memory leak where content overlays and feedback widgets remained referenced via lifecycle callbacks when the host activity finished.

## 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.
Expand Down Expand Up @@ -172,6 +182,8 @@
* During an internal timer tick
* Upon flushing the event queue

* Updated the internal request mechanism. Downgrading from this version is not recommended.

* Added support for array, List and JSONArray to all user given segmentations. They will support only mutable and ummutable versions of the primitive types. Which are:
* String, Integer, int, Boolean, bool, Float, float, Double, double, Long, long
* Keep in mind that float array will be converted to the double array by the JSONArray
Expand Down
11 changes: 11 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,24 @@ android {
}
}

buildFeatures {
// Required so `BuildConfig.COUNTLY_SERVER_URL` / `COUNTLY_APP_KEY`
// (declared below) get generated. AGP 8+ disables BuildConfig by default.
buildConfig true
}

defaultConfig {
applicationId "ly.count.android.demo"
minSdk 21
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

buildConfigField "String", "COUNTLY_SERVER_URL",
"\"${System.getenv('COUNTLY_SERVER_URL') ?: project.findProperty('countlyServerUrl') ?: 'https://your.server.ly'}\""
buildConfigField "String", "COUNTLY_APP_KEY",
"\"${System.getenv('COUNTLY_APP_KEY') ?: project.findProperty('countlyAppKey') ?: 'YOUR_APP_KEY'}\""
}
buildTypes {
debug {
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@
<activity
android:name=".ActivityExampleFeedback"
android:label="@string/activity_name_feedback"
android:configChanges="orientation|screenSize"/>
android:configChanges="orientation|screenSize"
android:exported="true"/>

<activity
android:name=".ActivityExampleContentZone"
android:label="@string/activity_name_content_zone"
android:configChanges="orientation|screenSize"/>
android:configChanges="orientation|screenSize"
android:exported="true"/>

<activity
android:name=".ActivityExampleFragments"
Expand Down
25 changes: 22 additions & 3 deletions app/src/main/java/ly/count/android/demo/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.os.Bundle;
import android.os.StrictMode;
import android.util.Log;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.google.android.gms.tasks.OnCompleteListener;
Expand All @@ -35,9 +36,9 @@
import static ly.count.android.sdk.messaging.CountlyPush.COUNTLY_BROADCAST_PERMISSION_POSTFIX;

public class App extends Application {
/** You should use try.count.ly instead of YOUR_SERVER for the line below if you are using Countly trial service */
private final static String COUNTLY_SERVER_URL = "https://your.server.ly";
private final static String COUNTLY_APP_KEY = "YOUR_APP_KEY";

private static String COUNTLY_SERVER_URL = "https://your.server.ly";
private static String COUNTLY_APP_KEY = "YOUR_APP_KEY";
private final static String DEFAULT_URL = "https://your.server.ly";
private final static String DEFAULT_APP_KEY = "YOUR_APP_KEY";

Expand All @@ -47,6 +48,24 @@ public class App extends Application {
public void onCreate() {
super.onCreate();

// Enable WebView remote debugging so the test runner can attach to the
// content/feedback widget's DOM via Chrome DevTools Protocol over an
// adb-forwarded socket. Process-wide flag — affects every WebView in
// this process, including the SDK's overlay WebView. Debug-only;
// BuildConfig.DEBUG is true on debug builds, false on release.
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
}

COUNTLY_SERVER_URL =
DEFAULT_URL.equals(BuildConfig.COUNTLY_SERVER_URL)
? DEFAULT_URL
: BuildConfig.COUNTLY_SERVER_URL;
COUNTLY_APP_KEY =
DEFAULT_APP_KEY.equals(BuildConfig.COUNTLY_APP_KEY)
? DEFAULT_APP_KEY
: BuildConfig.COUNTLY_APP_KEY;

if (DEFAULT_URL.equals(COUNTLY_SERVER_URL) || DEFAULT_APP_KEY.equals(COUNTLY_APP_KEY)) {
Log.e("CountlyDemo", "Please provide correct COUNTLY_SERVER_URL and COUNTLY_APP_KEY");
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package ly.count.android.demo

import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy.Builder
import android.os.StrictMode.VmPolicy
import android.os.strictmode.UntaggedSocketViolation
import android.os.strictmode.Violation
import android.util.Log
import java.util.concurrent.Executors

object StrictModeConfigurator {

private val penaltyExecutor by lazy { Executors.newSingleThreadExecutor() }

private val threadPolicy: StrictMode.ThreadPolicy
get() = Builder()
.detectAll()
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
penaltyListener(penaltyExecutor) { violation ->
val knownIssue = knownThreadViolations.any { it(violation) }
if (!knownIssue) Log.w("StrictMode", null, violation)
}
} else {
penaltyLog()
}
}
.penaltyDeathOnNetwork()
.build()

private val knownThreadViolations: List<Violation.() -> Boolean> by lazy {
listOf(
// add known violations if any
)
}

private val vmPolicy: VmPolicy
get() = VmPolicy.Builder()
.apply {
detectActivityLeaks()
detectLeakedSqlLiteObjects()
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
detectFileUriExposure()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
detectCleartextNetwork()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
detectContentUriWithoutPermission()
detectUntaggedSockets() // okhttp "issue"
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
detectCredentialProtectedWhileLocked()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
detectIncorrectContextUse() // countly has known issue
detectUnsafeIntentLaunch()
}
}
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
penaltyListener(penaltyExecutor) { violation ->
val knownIssue = knownVmViolations.any { it(violation) }
if (!knownIssue) Log.w("StrictMode", null, violation)
}
} else {
penaltyLog()
}
}
.penaltyDeathOnFileUriExposure()
.build()

private val knownVmViolations: List<Violation.() -> Boolean> by lazy {
listOfNotNull(
{
this is UntaggedSocketViolation && stackTrace.any {
it.className.contains("ImmediateRequestMaker") || it.className.contains("ConnectionProcessor") // countly
}
},
)
}

@JvmStatic
fun configure() {
StrictMode.setThreadPolicy(threadPolicy)
StrictMode.setVmPolicy(vmPolicy)
}
}
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.2
VERSION_NAME=26.1.3
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 @@ -539,9 +539,19 @@ public void createWindowParams_correctTypeAndFlags() {
Assert.assertEquals("Type should be TYPE_APPLICATION",
WindowManager.LayoutParams.TYPE_APPLICATION, params.type);

// Expected base flags match the production set in createWindowParams:
// FLAG_NOT_FOCUSABLE + FLAG_WATCH_OUTSIDE_TOUCH let the host
// activity keep IME focus while still receiving outside-touch
// events the overlay routes back via dispatchTouchEvent.
// FLAG_NOT_TOUCHABLE is added only while content is still loading
// (gates touches until the WebView is visible). The test
// constructs the overlay with about:blank and never waits for
// afterPageFinished, so isContentLoaded stays false here.
int expectedFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
Assert.assertEquals("Flags should match", expectedFlags, params.flags);

Expand Down Expand Up @@ -790,4 +800,63 @@ public void contentUrlAction_noQueryParams_returnsFalse() {
Assert.assertFalse(overlay.contentUrlAction(url, overlay.webView));
});
}

// ===================== Memory leak prevention (issue #556) =====================

/**
* Structural invariant: the overlay's View.mContext must not pin the
* constructing Activity. The overlay outlives activity transitions, and
* View.mContext can never be swapped after construction — if it's the
* Activity, that Activity stays GC-pinned for the overlay's full lifetime.
*
* The exact context type is API-dependent (see ContentOverlayView#resolveOverlayContext):
* - Pre-API 31: Application context.
* - API 31+: createConfigurationContext from the Activity — a ContextImpl
* wrapper that holds an IBinder token, not the Activity instance, so
* GC isn't blocked. Required for StrictMode#detectIncorrectContextUse.
*
* In both cases, getApplicationContext() resolves to the same Application.
* The test asserts both that the context is NOT the Activity and that it
* routes back to the right Application — which is the actual leak-avoidance
* contract independent of API level.
*/
@Test
public void constructor_usesApplicationContext_notActivity() {
withActivity(activity -> {
overlay = createOverlay(activity);
Assert.assertNotSame(
"ContentOverlayView.mContext must not be the constructing Activity — "
+ "View.mContext can never be swapped, so binding it to an Activity leaks "
+ "that Activity for the lifetime of the overlay.",
activity, overlay.getContext());
Assert.assertSame(
"ContentOverlayView.mContext must resolve to the same Application as the "
+ "constructing Activity (Application directly on <API 31, "
+ "ConfigurationContext-of-Activity on API 31+).",
activity.getApplicationContext(),
overlay.getContext().getApplicationContext());
});
}

/**
* Same invariant for the embedded WebView. Even with the wrapper View not
* pinning the Activity, a WebView constructed with the Activity directly
* would still pin it via its own mContext. See
* constructor_usesApplicationContext_notActivity for the API-level rationale.
*/
@Test
public void webView_usesApplicationContext_notActivity() {
withActivity(activity -> {
overlay = createOverlay(activity);
Assert.assertNotNull("WebView should be created during construction", overlay.webView);
Assert.assertNotSame(
"ContentOverlayView's WebView.mContext must not be the constructing Activity.",
activity, overlay.webView.getContext());
Assert.assertSame(
"ContentOverlayView's WebView.mContext must resolve to the same Application "
+ "as the constructing Activity.",
activity.getApplicationContext(),
overlay.webView.getContext().getApplicationContext());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ public class CountlyStoreExplicitModeTests {

@Before
public void setUp() {
// Reset the shared Countly singleton — without this, init() state from a prior
// test class in the suite leaks into our new Countly() instances and dirties the
// event/request caches before the "this should perform no write" assertions can
// measure them. The other suites (ContentOverlayViewTests,
// ModuleConfigurationTests, ...) do the same halt+clear in setUp; this test class
// was the odd one out and produced ordering-dependent flakes.
Countly.sharedInstance().halt();
TestUtils.getCountlyStore().clear();

Countly.sharedInstance().setLoggingEnabled(true);
store = new CountlyStore(TestUtils.getContext(), mock(ModuleLog.class), false);
sp = store;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ public void sessionDurationScenario_1() throws InterruptedException, JSONExcepti
countly.deviceId().changeWithoutMerge("ff"); // this will generate a request with "end_session", "session_duration" fields and reset duration + begin_session
assertEquals(6, TestUtils.getCurrentRQ().length); // not 5 anymore, it will send orientation event as well

TestUtils.validateRequest("ff_merge", TestUtils.map("old_device_id", "1234"), 1);
ModuleEventsTests.validateEventInRQ("ff_merge", "[CLY]_orientation", null, 1, 0.0d, 0.0d, "_CLY_", "_CLY_", "_CLY_", "_CLY_", 2, -1, 0, 1);
ModuleEventsTests.validateEventInRQ(TestUtils.commonDeviceId, "[CLY]_orientation", null, 1, 0.0d, 0.0d, "_CLY_", "_CLY_", "_CLY_", "_CLY_", 1, -1, 0, 1);
TestUtils.validateRequest("ff_merge", TestUtils.map("old_device_id", "1234"), 2);
ModuleUserProfileTests.validateUserProfileRequest("ff_merge", 3, 6, TestUtils.map(), TestUtils.map("prop2", 123, "prop1", "string", "prop3", false));
ModuleSessionsTests.validateSessionEndRequest(4, 3, "ff_merge");

Expand Down
Loading
Loading