diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1aaaf62..a93cd2424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## XX.XX.XX +* Improved Content refresh mechanics. + ## 25.4.9 * Added a new config option `disableViewRestartForManualRecording()` to disable auto close/restart behavior of manual views on app background/foreground actions. diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java index 625d43649..541be2fd7 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -1174,7 +1175,6 @@ private int[] setupTest_allFeatures(JSONObject serverConfig) { countlyConfig.metricProviderOverride = new MockedMetricProvider(); Countly.sharedInstance().init(countlyConfig); Countly.sharedInstance().moduleContent.CONTENT_START_DELAY_MS = 0; // make it zero to catch content immediate request - Countly.sharedInstance().moduleContent.REFRESH_CONTENT_ZONE_DELAY_MS = 0; // make it zero to catch content immediate request return counts; } @@ -1224,6 +1224,11 @@ private void immediateFlow_allFeatures() throws InterruptedException { Thread.sleep(1000); Countly.sharedInstance().contents().refreshContentZone(); // will add one more content immediate request + try { + // wait for refresh to complete + Countly.sharedInstance().moduleContent.refreshContentZoneInternalFuture.get(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } } private void feedbackFlow_allFeatures() { diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java index cfb6b315d..24c306db4 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionQueue.java @@ -887,6 +887,15 @@ void ensureExecutor() { * Should only be called if SDK is initialized */ public void tick() { + tick(false); + } + + /** + * This function blocks caller until RQ flushes, so be cautious when using it. + * + * @param forceFlushRQ if true, will block until RQ is fully flushed + */ + public void tick(boolean forceFlushRQ) { //todo enable later //assert storageProvider != null; if (backoff_.get()) { @@ -904,10 +913,29 @@ public void tick() { return; } + if (forceFlushRQ && connectionProcessorFuture_ != null && !connectionProcessorFuture_.isDone()) { + L.d("[ConnectionQueue] tick, forceFlushRQ ongoing future closing it"); + try { + connectionProcessorFuture_.get(); + cpDoneIfOngoing = true; + } catch (Exception e) { + L.e("[ConnectionQueue] tick, forceFlushRQ ongoing future encountered an error: " + e.getMessage()); + } + } + if (!rqEmpty && (connectionProcessorFuture_ == null || cpDoneIfOngoing)) { L.d("[ConnectionQueue] tick, Starting ConnectionProcessor"); - ensureExecutor(); - connectionProcessorFuture_ = executor_.submit(createConnectionProcessor()); + Runnable cp = createConnectionProcessor(); + if (forceFlushRQ) { + try { + cp.run(); + } catch (Exception e) { + L.e("[ConnectionQueue] tick, forceFlushRQ encountered an error: " + e.getMessage()); + } + } else { + ensureExecutor(); + connectionProcessorFuture_ = executor_.submit(cp); + } } } 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 0e5151bf2..5dd2b4ad2 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -12,11 +12,16 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; public class ModuleContent extends ModuleBase { private final ImmediateRequestGenerator iRGenerator; + private final ExecutorService refreshExecutor; // to not block main thread during refresh + Future refreshContentZoneInternalFuture; // for test access Content contentInterface; CountlyTimer countlyTimer; private boolean shouldFetchContents = false; @@ -25,12 +30,12 @@ public class ModuleContent extends ModuleBase { private final ContentCallback globalContentCallback; private int waitForDelay = 0; int CONTENT_START_DELAY_MS = 4000; // 4 seconds - int REFRESH_CONTENT_ZONE_DELAY_MS = 2500; // 2.5 seconds ModuleContent(@NonNull Countly cly, @NonNull CountlyConfig config) { super(cly, config); L.v("[ModuleContent] Initialising, zoneTimerInterval: [" + config.content.zoneTimerInterval + "], globalContentCallback: [" + config.content.globalContentCallback + "]"); iRGenerator = config.immediateRequestGenerator; + refreshExecutor = Executors.newSingleThreadExecutor(); contentInterface = new Content(); countlyTimer = new CountlyTimer(); @@ -295,6 +300,7 @@ void halt() { contentInterface = null; countlyTimer.stopTimer(L); countlyTimer = null; + refreshExecutor.shutdown(); } @Override @@ -329,13 +335,13 @@ private void refreshContentZoneInternal() { return; } - if (!shouldFetchContents) { - exitContentZoneInternal(); - } + exitContentZoneInternal(); - _cly.moduleRequestQueue.attemptToSendStoredRequestsInternal(); - - enterContentZoneInternal(null, REFRESH_CONTENT_ZONE_DELAY_MS); + refreshContentZoneInternalFuture = refreshExecutor.submit(() -> { + _cly.moduleRequestQueue.attemptToSendStoredRequestsInternal(true); + L.d("[ModuleContent] refreshContentZone, RQ flush done, re-entering content zone"); + enterContentZoneInternal(null, 0); + }); } public class Content { diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java b/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java index d446bee4e..fa0886ab0 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleRequestQueue.java @@ -197,7 +197,18 @@ public void flushQueuesInternal() { * attempt to process stored requests on demand */ public void attemptToSendStoredRequestsInternal() { - L.i("[ModuleRequestQueue] Calling attemptToSendStoredRequests"); + attemptToSendStoredRequestsInternal(false); + } + + /** + * This method sends all RQ synchronously if forceFlushRQ is true + * + * @param forceFlushRQ whether to force flush the request queue + * Be cautious when using this flag as it may cause ANRs if used on main thread + * Wrap calls in a separate thread to unsure non-Blocking UI or main thread + */ + protected void attemptToSendStoredRequestsInternal(boolean forceFlushRQ) { + L.i("[ModuleRequestQueue] attemptToSendStoredRequestsInternal, forceFlushRQ: [" + forceFlushRQ + "]"); //combine all available events into a request sendEventsIfNeeded(true); @@ -206,7 +217,7 @@ public void attemptToSendStoredRequestsInternal() { _cly.moduleUserProfile.saveInternal(); //trigger the processing of the request queue - requestQueueProvider.tick(); + requestQueueProvider.tick(forceFlushRQ); } /** diff --git a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java index 19fa17936..24fa38aab 100644 --- a/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java +++ b/sdk/src/main/java/ly/count/android/sdk/RequestQueueProvider.java @@ -52,6 +52,8 @@ interface RequestQueueProvider { void tick(); + void tick(boolean forceFlushRQ); + ConnectionProcessor createConnectionProcessor(); String prepareRemoteConfigRequestLegacy(@Nullable String keysInclude, @Nullable String keysExclude, @NonNull String preparedMetrics);