Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* it was compiled to Java 11 bytecode instead of Java 8, which older Android Gradle Plugin versions cannot read.
* it could only be used from projects compiling against SDK 34 or higher. `compileSdk` 24 is enough now, lower than any release since 23.8.0 accepted, and the minimum Android version is unchanged at 5.0 (API level 21). To get there the `androidx.lifecycle` dependency is now 2.3.1 and `androidx.annotation` 1.3.0, the newest versions that impose no compile floor and bring no Kotlin standard library with them.

* Fixed an issue where a view ID stopped working after the app was backgrounded and foregrounded. The view was reopened under a new ID, so `stopViewWithID`, `pauseViewWithID`, `resumeViewWithID`, and `addSegmentationToViewWithID` no longer recognized the ID returned by `startView`, and the view stayed open.

## 26.1.5
* The SDK now supports API level 37. Integrating apps must build with `compileSdk` 34 or higher.
* Added a new configuration option `setCustomSSLSocketFactory(SSLSocketFactory)` to send the SDK's HTTPS requests through a custom SSLSocketFactory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2149,6 +2149,40 @@ public void autoViewTracking_restartDisabledForManualViews() throws JSONExceptio
Assert.assertEquals(8, TestUtils.getCurrentRQ().length);
}

/**
* "startView" with a bg/fg switch
* - Validate that the ID returned by "startView" still resolves after the view was closed on background
* and reopened on foreground under a new ID
* - Validate that "stopViewWithID" with that ID closes the view instead of being ignored
* - Validate that no ID of a stopped view is kept around
*/
@Test
public void startView_viewIDResolvesAfterBackgroundForeground() {
CountlyConfig countlyConfig = TestUtils.createBaseConfig(TestUtils.getContext());
countlyConfig.setEventQueueSizeToSend(1);

Countly countly = new Countly().init(countlyConfig);

Activity activity = mock(Activity.class);
countly.onStart(activity);

String viewID = countly.views().startView("test");
Assert.assertTrue(countly.moduleViews.viewDataMap.containsKey(viewID));

countly.onStop();
countly.onStart(activity);

String currentViewID = countly.moduleViews.currentIDFor(viewID);
Assert.assertNotEquals(viewID, currentViewID);
Assert.assertTrue(countly.moduleViews.viewDataMap.containsKey(currentViewID));
Assert.assertFalse(countly.moduleViews.viewDataMap.containsKey(viewID));

countly.views().stopViewWithID(viewID);

Assert.assertTrue(countly.moduleViews.viewDataMap.isEmpty());
Assert.assertTrue(countly.moduleViews.restartedViewIDs.isEmpty());
}

static void validateView(String viewName, Double viewDuration, int idx, int size, boolean start, boolean visit, Map<String, Object> customSegmentation, String id, String pvid) throws JSONException {
validateView(viewName, viewDuration, idx, size, start, visit, customSegmentation, id, pvid, null);
}
Expand Down
75 changes: 74 additions & 1 deletion sdk/src/main/java/ly/count/android/sdk/ModuleViews.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public class ModuleViews extends ModuleBase implements ViewIdProvider {

final Map<String, ViewData> viewDataMap = new ConcurrentHashMap<>(); // map viewIDs to its viewData

//Maps every view ID that was handed out to the host to the ID its view is open under now. A view closed on
//background and reopened on foreground gets a fresh ID, so without this the ID the host saved stops resolving.
final Map<String, String> restartedViewIDs = new ConcurrentHashMap<>();

SafeIDGenerator safeViewIDGenerator;

String[] reservedSegmentationKeysViews = { "name", "visit", "start", "segment" };
Expand Down Expand Up @@ -153,6 +157,57 @@ Map<String, Object> CreateViewEventSegmentation(@NonNull ViewData vd, boolean fi
return viewSegmentation;
}

/**
* Resolves a view ID the host is holding into the ID its view is open under now.
*
* @param viewID String - view ID as it was handed out to the host
* @return String - the ID the view is currently tracked with, or the provided one if it was never restarted
*/
@NonNull String currentIDFor(@NonNull String viewID) {
String currentViewID = restartedViewIDs.get(viewID);

if (currentViewID == null) {
return viewID;
}

L.d("[ModuleViews] currentIDFor, view ID:[" + viewID + "] was restarted, resolving to:[" + currentViewID + "]");
return currentViewID;
}

/**
* Records that a view which was open under one ID is now open under another one, so that every ID the host
* was ever given for that view keeps resolving.
*
* @param oldViewID String - ID the view was open under before it was restarted
* @param newViewID String - ID the view is open under now
*/
void rememberRestartedView(@NonNull String oldViewID, @NonNull String newViewID) {
for (Map.Entry<String, String> entry : restartedViewIDs.entrySet()) {
if (oldViewID.equals(entry.getValue())) {
entry.setValue(newViewID);
}
}

restartedViewIDs.put(oldViewID, newViewID);
}

/**
* Drops every ID that resolves to the provided view ID, and the entry for the view ID itself. Called when a
* view is finally stopped and none of its IDs should resolve any more.
*
* @param viewID String - ID the view was open under when it was stopped
*/
void forgetRestartedView(@NonNull String viewID) {
Iterator<Map.Entry<String, String>> iterator = restartedViewIDs.entrySet().iterator();

while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
if (viewID.equals(entry.getKey()) || viewID.equals(entry.getValue())) {
iterator.remove();
}
}
}

void autoCloseRequiredViews(boolean closeAllViews, @Nullable Map<String, Object> customViewSegmentation) {
L.d("[ModuleViews] autoCloseRequiredViews");
List<ViewData> viewsToRemove = new ArrayList<>(1);
Expand All @@ -175,6 +230,7 @@ void autoCloseRequiredViews(boolean closeAllViews, @Nullable Map<String, Object>
} else if (closeAllViews) {
//if we are closing all views, we should remove the view from the cache
viewDataMap.remove(vd.viewID);
forgetRestartedView(vd.viewID);
}
}
}
Expand Down Expand Up @@ -279,6 +335,9 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map<String, Objec
L.e("[ModuleViews] stopViewWithNameInternal, Trying to record view with null or empty view ID, ignoring request");
return;
}

viewID = currentIDFor(viewID);

//todo extract common checks
if (!viewDataMap.containsKey(viewID)) {
L.w("[ModuleViews] stopViewWithIDInternal, there is no view with the provided view id to close");
Expand Down Expand Up @@ -308,6 +367,7 @@ void stopViewWithIDInternal(@Nullable String viewID, @Nullable Map<String, Objec

if (!vd.willStartAgain) {
viewDataMap.remove(vd.viewID);
forgetRestartedView(vd.viewID);
}
}

Expand Down Expand Up @@ -347,6 +407,8 @@ void pauseViewWithIDInternal(String viewID) {
return;
}

viewID = currentIDFor(viewID);

if (!viewDataMap.containsKey(viewID)) {
L.w("[ModuleViews] pauseViewWithIDInternal, there is no view with the provided view id to close");
return;
Expand Down Expand Up @@ -385,6 +447,8 @@ void resumeViewWithIDInternal(String viewID) {
return;
}

viewID = currentIDFor(viewID);

if (!viewDataMap.containsKey(viewID)) {
L.w("[ModuleViews] resumeViewWithIDInternal, there is no view with the provided view id to close");
return;
Expand Down Expand Up @@ -421,6 +485,8 @@ public void addSegmentationToViewWithIDInternal(@Nullable String viewID, @Nullab
return;
}

viewID = currentIDFor(viewID);

if (!viewDataMap.containsKey(viewID)) {
L.w("[ModuleViews] addSegmentationToViewWithID, there is no view with the provided view id");
return;
Expand Down Expand Up @@ -530,7 +596,14 @@ void startStoppedViews() {
if (vd.willStartAgain) {
//if the view is auto-stopped, start it again and remove from the cache
iterator.remove();
startViewInternal(vd.viewName, vd.viewSegmentation, vd.isAutoStoppedView);
String restartedViewID = startViewInternal(vd.viewName, vd.viewSegmentation, vd.isAutoStoppedView);

if (restartedViewID == null) {
//the view could not be reopened, so no ID the host holds for it points anywhere any more
forgetRestartedView(vd.viewID);
} else {
rememberRestartedView(vd.viewID, restartedViewID);
}
}
}
}
Expand Down
Loading