android-branch-deep-linking-attribution
android-branch-deep-linking-attribution copied to clipboard
[android] play console reports thousands of crashes due to branch
The Play Console reports thousands of crashes of our app due to Branch.
Crash reports:
java.lang.RuntimeException:
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:6864)
at android.app.ActivityThread.access$1300 (ActivityThread.java:268)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1982)
at android.os.Handler.dispatchMessage (Handler.java:107)
at android.os.Looper.loop (Looper.java:237)
at android.app.ActivityThread.main (ActivityThread.java:7814)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1075)
Caused by: java.lang.IllegalStateException:
at android.app.ContextImpl.getSharedPreferences (ContextImpl.java:485)
at android.app.ContextImpl.getSharedPreferences (ContextImpl.java:461)
at android.content.ContextWrapper.getSharedPreferences (ContextWrapper.java:184)
at io.branch.referral.PrefHelper.<init> (PrefHelper.java:172)
at io.branch.referral.PrefHelper.getInstance (PrefHelper.java:190)
at io.branch.referral.Branch.<init> (Branch.java:399)
at io.branch.referral.Branch.initInstance (Branch.java:791)
at io.branch.referral.Branch.getBranchInstance (Branch.java:624)
at io.branch.referral.Branch.getAutoInstance (Branch.java:695)
at com.example.CustomApplicationClass.onCreate (CustomApplicationClass.java:18)
at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1190)
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:6859)
java.lang.RuntimeException:
at android.app.ActivityThread.handleMakeApplication (ActivityThread.java:7189)
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:7134)
at android.app.ActivityThread.access$1600 (ActivityThread.java:274)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2102)
at android.os.Handler.dispatchMessage (Handler.java:107)
at android.os.Looper.loop (Looper.java:237)
at android.app.ActivityThread.main (ActivityThread.java:8167)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:496)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1100)
Caused by: java.lang.IllegalStateException:
at android.app.ContextImpl.getSharedPreferences (ContextImpl.java:486)
at android.app.ContextImpl.getSharedPreferences (ContextImpl.java:462)
at android.content.ContextWrapper.getSharedPreferences (ContextWrapper.java:184)
at io.branch.referral.PrefHelper.<init> (PrefHelper.java:172)
at io.branch.referral.PrefHelper.getInstance (PrefHelper.java:190)
at io.branch.referral.Branch.<init> (Branch.java:399)
at io.branch.referral.Branch.initInstance (Branch.java:791)
at io.branch.referral.Branch.getBranchInstance (Branch.java:624)
at io.branch.referral.Branch.getAutoInstance (Branch.java:695)
at com.example.CustomApplicationClass.onCreate (CustomApplicationClass.java:18)
at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1190)
at android.app.ActivityThread.handleMakeApplication (ActivityThread.java:7184)
and similar ones
Happens mostly on Android 10 (~70%), Android 9 (~20%) and Android 8.
I have tried using version 5.0.0 and 5.0.3, but results are the same.
My CustomApplicationClass
looks like this:
package com.example;
import android.content.Context;
import androidx.multidex.MultiDex;
import androidx.multidex.MultiDexApplication;
import io.branch.referral.Branch;
public class CustomApplicationClass extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
// Branch logging for debugging
Branch.enableLogging();
// Branch object initialization
Branch.getAutoInstance(getApplicationContext());
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
MainActivity
looks like this (simplified):
public class MainActivity extends AppCompatActivity {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
@Override public void onStart() {
super.onStart();
Uri data = getIntent() != null ? getIntent().getData() : null;
Branch.sessionBuilder(this).withCallback(callback).withData(data).init();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
this.setIntent(intent);
Branch.sessionBuilder(this).withCallback(callback).reInit();
}
private Branch.BranchReferralInitListener callback = new Branch.BranchReferralInitListener() {
@Override
public void onInitFinished(JSONObject referringParams, BranchError error) {
if (error == null) {
Log.i("BRANCH SDK", referringParams.toString());
} else {
Log.i("BRANCH SDK", error.getMessage());
}
}
};
}
So nothing strange there as far as I am aware. I suspect it really is an error in the Branch code.
I found the underlying problem, will share solution tomorrow when I have some time on my hands.
@tafelnl Were you able to replicate the crash on your device? Is there a specific action that is causing this? This is a huge issue for us.
Was a pain in the ass to debug and reproduce this really.
Debugging
First let's take a look at where this crash is triggered exactly. It starts in the Application class registered in AndroidManifest.xml
. In my example that is: CustomApplicationClass.java
. We can then see that the crash was produced by calling Branch.getAutoInstance(getApplicationContext());
. When we dig further into the error stack, we can find out that within the Branch SDK there is a helper class called PrefHelper.java
that can take care of storing the Branch API Key etc.
Within that class there is this line: context.getSharedPreferences(SHARED_PREF_FILE, Context.MODE_PRIVATE);
When we look at the docs of Android (https://developer.android.com/training/articles/direct-boot). We can see that they call this 'Credential encrypted storage'. This can only be accessed after the user has unlocked the device at least once after a reboot.
Now that we know this, the error makes a lot more sense as well. Because if we take a look at the getSharedPreferences
method in ContextImpl.java
file (where the error is finally triggered), we can see the following lines of code:
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
That means that before we try to get something from the store, we should check if the user is unlocked yes or no. Branch (purposely or not) decided to not do so in their code, which results to this error being triggered sometimes.
Again if we look at the Android docs under the section 'Get notified of user unlock', we can see that we can listen for an event and also check whether a user is locked or not.
Reproducing
- Open up the AVD Manager in Android Studio and make sure you have a device with at least SDK 27 (28 or 29 would be better)
- Run your app on that device
- Open your command prompt (with root/admin rights) and run
adb shell sm set-emulate-fbe true
(this will put the device in locked mode) - The device will probably be killed and closed automatically, otherwise close it yourself
- Re-run your app on that very same device
- Since it will now be user locked, the app will crash.
NOTE: I was not able to reproduce on an actual device in a nice way, so it is advised to use an emulator for this.
Workaround
So until Branch fixes this, we need to use a workaround
Changes in the CustomApplicationClass.java
As mentioned before, we should check if the user is unlocked before we try to access the preferences. For now we could do that by only calling getAutoInstance
when a user is unlocked:
public class CustomApplicationClass extends MultiDexApplication {
@Override
public void onCreate() {
super.onCreate();
// Branch logging for debugging
Branch.enableLogging();
+ Boolean isUnlocked = isUserUnlocked(getApplicationContext());
+ if (isUnlocked) {
// Branch object initialization
Branch.getAutoInstance(getApplicationContext());
+ }
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
+ public static boolean isUserUnlocked(@NonNull Context context) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
+ return ((UserManager) Objects.requireNonNull(context.getSystemService(Context.USER_SERVICE))).isUserUnlocked();
+ } else {
+ return true;
+ }
+ }
}
Changes in the MainActivity.java
The changes done in CustomApplicationClass.jave
means that a Branch instance will not always exist. So to prevent other crashes, we will have to check if an instance exists before we do anything with the Branch SDK. I.e. in MainActivity
this will result in the following changes:
public class MainActivity extends AppCompatActivity {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
@Override public void onStart() {
super.onStart();
Uri data = getIntent() != null ? getIntent().getData() : null;
+ Branch branch = Branch.getInstance();
+ if (branch != null} {
Branch.sessionBuilder(this).withCallback(callback).withData(data).init();
+ }
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
this.setIntent(intent);
+ Branch branch = Branch.getInstance();
+ if (branch != null} {
Branch.sessionBuilder(this).withCallback(callback).reInit();
+ }
}
private Branch.BranchReferralInitListener callback = new Branch.BranchReferralInitListener() {
@Override
public void onInitFinished(JSONObject referringParams, BranchError error) {
if (error == null) {
Log.i("BRANCH SDK", referringParams.toString());
} else {
Log.i("BRANCH SDK", error.getMessage());
}
}
};
}
@tafelnl Wow! Thank you so much.
Based on what you are describing it seems as if the app would be crashing in the background? Not while the user is using the app? Also, this seems like kind of an edge case scenario. Most users would open the app immediately after downloading it which would give them the correct permission, no? Then the only time this would possibly occur again would be if they restarted their device and didn't unlock it?
We are getting thousands of crashes daily. So it seems unlikely that this edge case would be happening that often.
Maybe I'm not getting the full picture.
Thanks so much for your help with this.
Yes, this means that likely most (if not all) app crashes happen in the background. But our apps do not have any background processes, so that is what I am struggling with as well.
But I think that it happens if another library in your codes listens to those Direct Boot Mode events. In my app, for example, I found a library that had the following code in it:
<receiver
android:name="com.example.plugin"
android:directBootAware="true" >
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
Keywords here to search your project for are: android:directBootAware="true"
, LOCKED_BOOT_COMPLETED
and BOOT_COMPLETED
. I suspect that this broadcast receiver will (at least partly) boot up your app to fulfill this listener. Since the Branch SDK is not configured (yet) for this kind of behaviour, it will crash.
Our apps that do not include such a receiver, also do not report any crashes at all. So I really suspect this is it, but I cannot say for sure yet.
So search your whole app (including libraries) for android:directBootAware="true"
, LOCKED_BOOT_COMPLETED
and BOOT_COMPLETED
. If you encounter those as well for the apps that report crashes, I think we have located our root cause.
Related SO thread: https://stackoverflow.com/a/56088069
I found a reference to
<receiver android:name="com.getcapacitor.plugin.notification.LocalNotificationRestoreReceiver" android:directBootAware="true" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
In the @capactior/android node module.
/node_modules/@capacitor/android/capacitor/src/main/AndroidManifest.xml
Yeah I really think that's the cause of the crashes. But in either case, the workaround described above should fix it. I did not yet release a production version of the app with that fix in it, but will do on a very short notice. I hope and believe that the crashes will be over then.
I added instructions to the 'Reproducing' section of my comment above.
@selected-pixel-jameson I released a new version a few days ago. No crashes for the version of the app have been reported since! So that means the workaround above should work for you too.
Awesome. We are planning on releasing today.
@tafelnl
public class MainActivity extends AppCompatActivity {
@Override public void onStart() { super.onStart(); Uri data = getIntent() != null ? getIntent().getData() : null; Branch branch = Branch.getInstance(); if (branch != null} { Branch.sessionBuilder(this).withCallback(callback).withData(data).init(); } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); this.setIntent(intent); Branch branch = Branch.getInstance(); if (branch != null} { Branch.sessionBuilder(this).withCallback(callback).reInit(); } }
}
Calling the sessionBuilder at start and newIntent isn't what's listed in the capacitor setup documentation.
https://help.branch.io/developers-hub/docs/capacitor
Is this meant to be an alternative to the add(BranchDeepLinks.class);
line in the onCreate method? You're calling branch manually instead of letting it create its own hooks in the init()
or something?
No. You are correct. Normally this would be handled automatically in https://github.com/BranchMetrics/capacitor-branch-deep-links/blob/master/android/src/main/java/co/boundstate/BranchDeepLinks.java
So that library should be edited to support for this. If I have some time on my hands sometime soon, I will create a few PR's in https://github.com/BranchMetrics/capacitor-branch-deep-links to improve some things there.
It appears Branch has added a fix for this in their documentation - https://help.branch.io/developers-hub/docs/capacitor ` public class CustomApplicationClass extends MultiDexApplication {
@Override public void onCreate() { super.onCreate();
// Branch logging for debugging
Branch.enableLogging();
// Branches fix for checking if user has unlocked the device
if (SDK_INT >= 24) {
UserManager um = getApplicationContext().getSystemService(UserManager.class);
if (um == null || !um.isUserUnlocked()) return;
}
// Branch object initialization
Branch.getAutoInstance(this);
} `
@crabbydavis That's based on a PR of mine actually, but it doesn't fix the root issue in this library. IMO it should be fixed within this library, and not be left up to the developer
I apologize in advance for this rant, but this company is pretty much worthless. We have stopped using their deep links all together because of the issues we had with them. Trying to get deep links to work correctly through our email campaign service was a complete joke. Their support is absolutely horrendous. The fact that this library has not been updated is just a testament to how horrible of a business they are. I would like to note we were also paying for their service.
If you are looking to use deep links avoid this service at all cost. Sorry I don’t have a different solution to offer.
On Oct 7, 2022, at 8:32 AM, Tafel @.***> wrote:
@crabbydavis https://github.com/crabbydavis That's based on a PR of mine actually, but it doesn't fix the main issue in this library. IMO it should be fixed within this library, and not be left up to the developer
— Reply to this email directly, view it on GitHub https://github.com/BranchMetrics/android-branch-deep-linking-attribution/issues/879#issuecomment-1271599997, or unsubscribe https://github.com/notifications/unsubscribe-auth/AGXF36NMGPD3NAJXQYVJL2TWCARAJANCNFSM4UNHLDDQ. You are receiving this because you were mentioned.
@selected-pixel-jameson, Could you please provide us with your support case ticket number so we can follow up there?
Nope. I’ve already wasted enough of my time with Branch.
On Oct 11, 2022, at 8:03 AM, Justin - Branch @.***> wrote:
@selected-pixel-jameson https://github.com/selected-pixel-jameson, Could you please provide us with your support case ticket number so we can follow up there?
— Reply to this email directly, view it on GitHub https://github.com/BranchMetrics/android-branch-deep-linking-attribution/issues/879#issuecomment-1274655840, or unsubscribe https://github.com/notifications/unsubscribe-auth/AGXF36L4AE6NHKCI6NIY5HLWCVQS3ANCNFSM4UNHLDDQ. You are receiving this because you were mentioned.