Skip to content

Commit

Permalink
Move optional synchronous user agent fetch out of start up (#1181)
Browse files Browse the repository at this point in the history
* Move synchronous user agent fetch out of start up

* move sync call to main dispatcher

* Update CustomBranchApp.java

* Update DeviceInfo.java

* Update DeviceInfo.java

* add else case to add object
  • Loading branch information
gdeluna-branch authored Mar 27, 2024
1 parent 1b6bb47 commit 72e3d1d
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -704,8 +724,6 @@ public void onInitFinished(JSONObject referringParams, BranchError error) {
}
}
}).reInit();


}

@Override
Expand Down
84 changes: 84 additions & 0 deletions Branch-SDK/src/main/java/io/branch/coroutines/DeviceSignals.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 6 additions & 7 deletions Branch-SDK/src/main/java/io/branch/referral/Branch.java
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
Expand Down Expand Up @@ -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_;
}

Expand Down Expand Up @@ -778,7 +773,11 @@ public static boolean isReferringLinkAttributionForPreinstalledAppsEnabled() {
public static void setIsUserAgentSync(boolean sync){
userAgentSync = sync;
}


public static boolean getIsUserAgentSync(){
return userAgentSync;
}

/*
* <p>Closes the current session. Should be called by on getting the last actvity onStop() event.
* </p>
Expand Down
162 changes: 91 additions & 71 deletions Branch-SDK/src/main/java/io/branch/referral/DeviceInfo.java
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
Expand Down Expand Up @@ -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(),
Expand All @@ -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<String>() {
@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<String>() {
@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
*
Expand Down Expand Up @@ -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
*/
Expand All @@ -364,6 +386,4 @@ SystemObserver getSystemObserver() {
public static boolean isNullOrEmptyOrBlank(String str) {
return TextUtils.isEmpty(str) || str.equals(SystemObserver.BLANK);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 72e3d1d

Please sign in to comment.