BranchMetrics / android-branch-deep-linking-attribution

The Branch Android SDK for deep linking and attribution. Branch helps mobile apps grow with deep links / deeplinks that power paid acquisition and re-engagement campaigns, referral programs, content sharing, deep linked emails, smart banners, custom user onboarding, and more.
https://docs.branch.io/pages/apps/android/
MIT License
401 stars 156 forks source link

[android] play console reports thousands of crashes due to branch #879

Open tafelnl opened 3 years ago

tafelnl commented 3 years ago

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.

tafelnl commented 3 years ago

I found the underlying problem, will share solution tomorrow when I have some time on my hands.

selected-pixel-jameson commented 3 years ago

@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.

tafelnl commented 3 years ago

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

  1. 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)
  2. Run your app on that device
  3. 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)
  4. The device will probably be killed and closed automatically, otherwise close it yourself
  5. Re-run your app on that very same device
  6. 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());
            }
        }
    };

}
selected-pixel-jameson commented 3 years ago

@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.

tafelnl commented 3 years ago

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.

tafelnl commented 3 years ago

Related SO thread: https://stackoverflow.com/a/56088069

selected-pixel-jameson commented 3 years ago

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

tafelnl commented 3 years ago

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.

tafelnl commented 3 years ago

I added instructions to the 'Reproducing' section of my comment above.

tafelnl commented 3 years ago

@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.

selected-pixel-jameson commented 3 years ago

Awesome. We are planning on releasing today.

mattsteve commented 3 years ago

@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?

tafelnl commented 3 years ago

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.

crabbydavis commented 2 years ago

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);

} `

tafelnl commented 2 years ago

@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

selected-pixel-jameson commented 2 years ago

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.

jf-branch commented 2 years ago

@selected-pixel-jameson, Could you please provide us with your support case ticket number so we can follow up there?

selected-pixel-jameson commented 2 years ago

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.