diff --git a/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/MainActivity.java b/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/MainActivity.java index 09f976d8e..97e4ed559 100644 --- a/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/MainActivity.java +++ b/Branch-SDK-TestBed/src/main/java/io/branch/branchandroidtestbed/MainActivity.java @@ -654,6 +654,35 @@ protected void onStart() { Branch.getInstance().addFacebookPartnerParameterWithName("em", getHashedValue("sdkadmin@branch.io")); Branch.getInstance().addFacebookPartnerParameterWithName("ph", getHashedValue("6516006060")); Log.d("BranchSDK_Tester", "initSession"); + + initSessionsWithTests(); + + // Branch integration validation: Validate Branch integration with your app + // NOTE : The below method will run few checks for verifying correctness of the Branch integration. + // Please look for "BranchSDK_Doctor" in the logcat to see the results. + // IMP : Do not make this call in your production app + + //IntegrationValidator.validate(MainActivity.this); + } + + + private void initSessionsWithTests() { + boolean testUserAgent = true; + userAgentTests(testUserAgent, 10); + } + + // Enqueue several v2 events prior to init to simulate worst timing conditions for user agent fetch + // TODO Add to automation. + // Check that all events up to Event N-1 complete with user agent string. + private void userAgentTests(boolean userAgentSync, int n) { + Branch.setIsUserAgentSync(userAgentSync); + Log.i("BranchSDK_Tester", "Beginning stress tests with IsUserAgentSync" + Branch.getIsUserAgentSync()); + + for (int i = 0; i < n; i++) { + BranchEvent event = new BranchEvent("Event " + i); + event.logEvent(this); + } + Branch.sessionBuilder(this).withCallback(new Branch.BranchUniversalReferralInitListener() { @Override public void onInitFinished(BranchUniversalObject branchUniversalObject, LinkProperties linkProperties, BranchError error) { @@ -677,17 +706,8 @@ public void onInitFinished(BranchUniversalObject branchUniversalObject, LinkProp // QA purpose only // TrackingControlTestRoutines.runTrackingControlTest(MainActivity.this); // BUOTestRoutines.TestBUOFunctionalities(MainActivity.this); - } }).withData(this.getIntent().getData()).init(); - - // Branch integration validation: Validate Branch integration with your app - // NOTE : The below method will run few checks for verifying correctness of the Branch integration. - // Please look for "BranchSDK_Doctor" in the logcat to see the results. - // IMP : Do not make this call in your production app - - //IntegrationValidator.validate(MainActivity.this); - } @Override @@ -704,8 +724,6 @@ public void onInitFinished(JSONObject referringParams, BranchError error) { } } }).reInit(); - - } @Override diff --git a/Branch-SDK/src/main/java/io/branch/coroutines/DeviceSignals.kt b/Branch-SDK/src/main/java/io/branch/coroutines/DeviceSignals.kt new file mode 100644 index 000000000..9a60ad904 --- /dev/null +++ b/Branch-SDK/src/main/java/io/branch/coroutines/DeviceSignals.kt @@ -0,0 +1,84 @@ +package io.branch.coroutines + +import android.content.Context +import android.text.TextUtils +import android.webkit.WebSettings +import android.webkit.WebView +import io.branch.referral.Branch +import io.branch.referral.BranchLogger.e +import io.branch.referral.BranchLogger.v +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +val mutex = Mutex() + +/** + * Returns the user agent string on a background thread via static class WebSettings + * This is the default behavior. + * + * Use a mutex to ensure only one is executed at a time. + * Successive calls will return the cached value. + * + * For performance, this is called at the end of the init, or while awaiting init if enqueued prior. + */ +suspend fun getUserAgentAsync(context: Context): String? { + return withContext(Dispatchers.Default) { + mutex.withLock { + var result: String? = null + + if (!TextUtils.isEmpty(Branch._userAgentString)) { + v("UserAgent cached " + Branch._userAgentString) + result = Branch._userAgentString + } + else { + try { + v("Begin getUserAgentAsync " + Thread.currentThread()) + result = WebSettings.getDefaultUserAgent(context) + v("End getUserAgentAsync " + Thread.currentThread() + " " + result) + } + catch (exception: Exception) { + e("Failed to retrieve userAgent string. " + exception.message) + } + } + + result + } + } +} + +/** + * Returns the user agent string on the main thread via WebView instance. + * Use when facing errors with the WebSettings static API. + * https://bugs.chromium.org/p/chromium/issues/detail?id=1279562 + * https://bugs.chromium.org/p/chromium/issues/detail?id=1271617 + * + * + * Because there is only one main thread, this function will only execute one at a time. + * Successive calls will return the cached value. + */ +suspend fun getUserAgentSync(context: Context): String?{ + return withContext(Dispatchers.Main){ + var result: String? = null + + if(!TextUtils.isEmpty(Branch._userAgentString)){ + v("UserAgent cached " + Branch._userAgentString) + result = Branch._userAgentString + } + else { + try { + v("Begin getUserAgentSync " + Thread.currentThread()) + val w = WebView(context) + result = w.settings.userAgentString + w.destroy() + v("End getUserAgentSync " + Thread.currentThread() + " " + result) + } + catch (ex: Exception) { + e("Failed to retrieve userAgent string. " + ex.message) + } + } + + result + } +} \ No newline at end of file diff --git a/Branch-SDK/src/main/java/io/branch/referral/Branch.java b/Branch-SDK/src/main/java/io/branch/referral/Branch.java index d9dc6382b..62166df39 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/Branch.java +++ b/Branch-SDK/src/main/java/io/branch/referral/Branch.java @@ -196,7 +196,7 @@ public class Branch { /** * Package private user agent string cached to save on repeated queries */ - static String _userAgentString = ""; + public static String _userAgentString = ""; /* Json object containing key-value pairs for debugging deep linking */ private JSONObject deeplinkDebugParams_; @@ -355,11 +355,6 @@ synchronized private static Branch initBranchSDK(@NonNull Context context, Strin branchReferral_.setActivityLifeCycleObserver((Application) context); } - // Cache the user agent from a webview instance if needed - if(userAgentSync && DeviceInfo.getInstance() != null){ - DeviceInfo.getInstance().getUserAgentStringSync(context); - } - return branchReferral_; } @@ -778,7 +773,11 @@ public static boolean isReferringLinkAttributionForPreinstalledAppsEnabled() { public static void setIsUserAgentSync(boolean sync){ userAgentSync = sync; } - + + public static boolean getIsUserAgentSync(){ + return userAgentSync; + } + /* *
Closes the current session. Should be called by on getting the last actvity onStop() event. *
diff --git a/Branch-SDK/src/main/java/io/branch/referral/DeviceInfo.java b/Branch-SDK/src/main/java/io/branch/referral/DeviceInfo.java index ff814e9b8..14f5c8a68 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/DeviceInfo.java +++ b/Branch-SDK/src/main/java/io/branch/referral/DeviceInfo.java @@ -1,25 +1,23 @@ package io.branch.referral; +import static android.content.Context.UI_MODE_SERVICE; +import static io.branch.referral.PrefHelper.NO_STRING_VALUE; + import android.app.UiModeManager; import android.content.Context; import android.content.res.Configuration; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.text.TextUtils; import android.util.DisplayMetrics; -import android.util.Log; -import android.webkit.WebSettings; -import android.webkit.WebView; + +import androidx.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; -import static android.content.Context.UI_MODE_SERVICE; -import static io.branch.referral.PrefHelper.NO_STRING_VALUE; - -import java.util.Iterator; -import java.util.Objects; +import io.branch.coroutines.DeviceSignalsKt; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.CoroutineContext; +import kotlin.coroutines.EmptyCoroutineContext; /** *
@@ -215,7 +213,8 @@ void updateRequestWithV2Params(ServerRequest serverRequest, PrefHelper prefHelpe
userDataObj.put(Defines.Jsonkey.AppVersion.getKey(), getAppVersion());
userDataObj.put(Defines.Jsonkey.SDK.getKey(), "android");
userDataObj.put(Defines.Jsonkey.SdkVersion.getKey(), Branch.getSdkVersionNumber());
- userDataObj.put(Defines.Jsonkey.UserAgent.getKey(), getDefaultBrowserAgent(context_));
+
+ setPostUserAgent(userDataObj);
if (serverRequest instanceof ServerRequestGetLATD) {
userDataObj.put(Defines.Jsonkey.LATDAttributionWindow.getKey(),
@@ -236,6 +235,86 @@ void updateRequestWithV2Params(ServerRequest serverRequest, PrefHelper prefHelpe
}
}
+ /**
+ * Method to append the user agent string to the POST request body's user_data object
+ * If the user agent string is empty, either because it was not obtained asynchronously
+ * or on time, query it synchronously.
+ * @param userDataObj
+ */
+ private void setPostUserAgent(final JSONObject userDataObj) {
+ BranchLogger.v("setPostUserAgent " + Thread.currentThread().getName());
+ try {
+ if (!TextUtils.isEmpty(Branch._userAgentString)) {
+ BranchLogger.v("userAgent was cached: " + Branch._userAgentString);
+
+ userDataObj.put(Defines.Jsonkey.UserAgent.getKey(), Branch._userAgentString);
+
+ Branch.getInstance().requestQueue_.unlockProcessWait(ServerRequest.PROCESS_WAIT_LOCK.USER_AGENT_STRING_LOCK);
+ Branch.getInstance().requestQueue_.processNextQueueItem("setPostUserAgent");
+ }
+ else if (Branch.userAgentSync) {
+ // If user agent sync is false, then the async coroutine is executed instead but may not have finished yet.
+ BranchLogger.v("Start invoking getUserAgentSync from thread " + Thread.currentThread().getName());
+ DeviceSignalsKt.getUserAgentSync(context_, new Continuation
@@ -89,6 +95,33 @@ public void onRequestSucceeded(ServerResponse response, Branch branch) {
void onInitSessionCompleted(ServerResponse response, Branch branch) {
DeepLinkRoutingValidator.validate(branch.currentActivityReference_);
branch.updateSkipURLFormats();
+
+ // Run this after session init, ahead of any V2 event, in the background.
+ if (!Branch.userAgentSync && !TextUtils.isEmpty(Branch._userAgentString)) {
+ DeviceSignalsKt.getUserAgentAsync(branch.getApplicationContext(), new Continuation