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() { + @NonNull + @Override + public CoroutineContext getContext() { + return EmptyCoroutineContext.INSTANCE; + } + + @Override + public void resumeWith(@NonNull Object o) { + if (o != null) { + Branch._userAgentString = (String) o; + BranchLogger.v("onUserAgentStringFetchFinished getUserAgentSync resumeWith releasing lock"); + + try { + userDataObj.put(Defines.Jsonkey.UserAgent.getKey(), Branch._userAgentString); + } + catch (JSONException e) { + BranchLogger.w("Caught JSONException " + e.getMessage()); + } + } + + Branch.getInstance().requestQueue_.unlockProcessWait(ServerRequest.PROCESS_WAIT_LOCK.USER_AGENT_STRING_LOCK); + Branch.getInstance().requestQueue_.processNextQueueItem("onUserAgentStringFetchFinished"); + } + }); + } + // In cases where v2 events objects are enqueued before an init, this will execute first. + else { + DeviceSignalsKt.getUserAgentAsync(context_, new Continuation() { + @NonNull + @Override + public CoroutineContext getContext() { + return EmptyCoroutineContext.INSTANCE; + } + + @Override + public void resumeWith(@NonNull Object o) { + if (o != null) { + Branch._userAgentString = (String) o; + BranchLogger.v("onUserAgentStringFetchFinished getUserAgentAsync resumeWith releasing lock"); + + try { + userDataObj.put(Defines.Jsonkey.UserAgent.getKey(), Branch._userAgentString); + } + catch (JSONException e) { + BranchLogger.w("Caught JSONException " + e.getMessage()); + } + } + + Branch.getInstance().requestQueue_.unlockProcessWait(ServerRequest.PROCESS_WAIT_LOCK.USER_AGENT_STRING_LOCK); + Branch.getInstance().requestQueue_.processNextQueueItem("getUserAgentAsync resumeWith"); + } + }); + } + } + catch (Exception exception){ + BranchLogger.w("Caught exception trying to set userAgent " + exception.getMessage()); + } + } + /** * get the package name for the this application * @@ -288,63 +367,6 @@ public String getOsName() { return systemObserver_.getOS(context_); } - - /** - * Returns the browser's user agent string - * PRS : User agent is checked only from api-17 - * @param context - * @return user agent string - */ - String getDefaultBrowserAgent(final Context context) { - if(!TextUtils.isEmpty(Branch._userAgentString)) { - return Branch._userAgentString; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - try { - BranchLogger.v("Retrieving user agent string from WebSettings"); - Branch._userAgentString = WebSettings.getDefaultUserAgent(context); - } - catch (Exception exception) { - BranchLogger.v(exception.getMessage()); - // A known Android issue. Webview packages are not accessible while any updates for chrome is in progress. - // https://bugs.chromium.org/p/chromium/issues/detail?id=506369 - } - } - return Branch._userAgentString; - } - - /** - * Must be called from the main thread - * Some devices appear to crash when accessing chromium through the Android framework statics - * Suggested alternative is to use a webview instance - * https://bugs.chromium.org/p/chromium/issues/detail?id=1279562 - * https://bugs.chromium.org/p/chromium/issues/detail?id=1271617 - **/ - String getUserAgentStringSync(final Context context){ - if(!TextUtils.isEmpty(Branch._userAgentString)) { - return Branch._userAgentString; - } - - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - try { - BranchLogger.v("Running WebView initialization for user agent on thread " + Thread.currentThread()); - WebView w = new WebView(context); - Branch._userAgentString = w.getSettings().getUserAgentString(); - w.destroy(); - } - catch (Exception e) { - BranchLogger.v(e.getMessage()); - } - - } - }); - - return Branch._userAgentString; - } - /** * Concrete SystemObserver implementation */ @@ -364,6 +386,4 @@ SystemObserver getSystemObserver() { public static boolean isNullOrEmptyOrBlank(String str) { return TextUtils.isEmpty(str) || str.equals(SystemObserver.BLANK); } - - } diff --git a/Branch-SDK/src/main/java/io/branch/referral/ServerRequest.java b/Branch-SDK/src/main/java/io/branch/referral/ServerRequest.java index bdb06c199..d3c256504 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/ServerRequest.java +++ b/Branch-SDK/src/main/java/io/branch/referral/ServerRequest.java @@ -42,8 +42,8 @@ public abstract class ServerRequest { private final Context context_; // Various process wait locks for Branch server request - enum PROCESS_WAIT_LOCK { - SDK_INIT_WAIT_LOCK, GAID_FETCH_WAIT_LOCK, INTENT_PENDING_WAIT_LOCK, USER_SET_WAIT_LOCK, INSTALL_REFERRER_FETCH_WAIT_LOCK + public enum PROCESS_WAIT_LOCK { + SDK_INIT_WAIT_LOCK, GAID_FETCH_WAIT_LOCK, INTENT_PENDING_WAIT_LOCK, USER_SET_WAIT_LOCK, INSTALL_REFERRER_FETCH_WAIT_LOCK, USER_AGENT_STRING_LOCK } // Set for holding any active wait locks diff --git a/Branch-SDK/src/main/java/io/branch/referral/ServerRequestInitSession.java b/Branch-SDK/src/main/java/io/branch/referral/ServerRequestInitSession.java index f661a9ded..ab96b973c 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/ServerRequestInitSession.java +++ b/Branch-SDK/src/main/java/io/branch/referral/ServerRequestInitSession.java @@ -3,10 +3,16 @@ import android.content.Context; import android.text.TextUtils; +import androidx.annotation.NonNull; + import org.json.JSONException; import org.json.JSONObject; +import io.branch.coroutines.DeviceSignalsKt; import io.branch.referral.validators.DeepLinkRoutingValidator; +import kotlin.coroutines.Continuation; +import kotlin.coroutines.CoroutineContext; +import kotlin.coroutines.EmptyCoroutineContext; /** *

@@ -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() { + @NonNull + @Override + public CoroutineContext getContext() { + return EmptyCoroutineContext.INSTANCE; + } + + @Override + public void resumeWith(@NonNull Object o) { + if (o != null) { + BranchLogger.v("onInitSessionCompleted resumeWith userAgent " + o + " on thread " + Thread.currentThread().getName()); + Branch._userAgentString = (String) o; + } + + Branch.getInstance().requestQueue_.unlockProcessWait(PROCESS_WAIT_LOCK.USER_AGENT_STRING_LOCK); + Branch.getInstance().requestQueue_.processNextQueueItem("getUserAgentAsync resumeWith"); + } + }); + } + else { + BranchLogger.v("Deferring userAgent string call for sync retrieval"); + } + + BranchLogger.v("onInitSessionCompleted on thread " + Thread.currentThread().getName()); } /** diff --git a/Branch-SDK/src/main/java/io/branch/referral/ServerRequestQueue.java b/Branch-SDK/src/main/java/io/branch/referral/ServerRequestQueue.java index 02fd4967c..d892d7a10 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/ServerRequestQueue.java +++ b/Branch-SDK/src/main/java/io/branch/referral/ServerRequestQueue.java @@ -7,6 +7,7 @@ import android.content.SharedPreferences; import android.os.Handler; import android.os.Looper; +import android.text.TextUtils; import androidx.annotation.Nullable; diff --git a/Branch-SDK/src/main/java/io/branch/referral/util/BranchEvent.java b/Branch-SDK/src/main/java/io/branch/referral/util/BranchEvent.java index b0b867368..3dda5da40 100644 --- a/Branch-SDK/src/main/java/io/branch/referral/util/BranchEvent.java +++ b/Branch-SDK/src/main/java/io/branch/referral/util/BranchEvent.java @@ -1,6 +1,7 @@ package io.branch.referral.util; import android.content.Context; +import android.text.TextUtils; import org.json.JSONException; import org.json.JSONObject; @@ -12,7 +13,9 @@ import io.branch.indexing.BranchUniversalObject; import io.branch.referral.Branch; +import io.branch.referral.BranchLogger; import io.branch.referral.Defines; +import io.branch.referral.ServerRequest; import io.branch.referral.ServerRequestLogEvent; import io.branch.referral.ServerResponse; @@ -262,32 +265,39 @@ public interface BranchLogEventCallback { public boolean logEvent(Context context, final BranchLogEventCallback callback) { boolean isReqQueued = false; Defines.RequestPath reqPath = isStandardEvent ? Defines.RequestPath.TrackStandardEvent : Defines.RequestPath.TrackCustomEvent; + if (Branch.getInstance() != null) { - Branch.getInstance().requestQueue_.handleNewRequest( - new ServerRequestLogEvent(context, reqPath, eventName, topLevelProperties, standardProperties, customProperties, buoList) { - @Override - public void onRequestSucceeded(ServerResponse response, Branch branch) { - if (callback != null) { - callback.onSuccess(response.getStatusCode()); - } - } - - @Override - public void handleFailure(int statusCode, String causeMsg) { - if (callback != null) { - Exception e = new Exception("Failed logEvent server request: " + statusCode + causeMsg); - callback.onFailure(e); - } - } + ServerRequest req = new ServerRequestLogEvent(context, reqPath, eventName, topLevelProperties, standardProperties, customProperties, buoList) { + @Override + public void onRequestSucceeded(ServerResponse response, Branch branch) { + if (callback != null) { + callback.onSuccess(response.getStatusCode()); + } + } + + @Override + public void handleFailure(int statusCode, String causeMsg) { + if (callback != null) { + Exception e = new Exception("Failed logEvent server request: " + statusCode + causeMsg); + callback.onFailure(e); } - ); + } + }; + + BranchLogger.v("Preparing V2 event, user agent is " + Branch._userAgentString); + + if(TextUtils.isEmpty(Branch._userAgentString)){ + BranchLogger.v("handleNewRequest adding process wait lock USER_AGENT_STRING_LOCK"); + req.addProcessWaitLock(ServerRequest.PROCESS_WAIT_LOCK.USER_AGENT_STRING_LOCK); + } + + Branch.getInstance().requestQueue_.handleNewRequest(req); isReqQueued = true; - } else if (callback != null) { + } + else if (callback != null) { Exception e = new Exception("Failed logEvent server request: The Branch instance was not available"); callback.onFailure(e); } return isReqQueued; } - - }