3

I have the following leak detected by LeakCanary where it appears that:

GC ROOT android.hardware.fingerprint.FingerprintManager$1.this$0 (anonymous subclass of android.hardware.fingerprint.IFingerprintServiceReceiver$Stub) references android.hardware.fingerprint.FingerprintManager.mContext leaks com.alga.com.mohammed.views PasscodeActivity instance

Drocchio
  • 383
  • 4
  • 21

2 Answers2

3

Try replacing:

val fingerprintManagerInstance = this.getSystemService(FINGERPRINT_SERVICE) ?: return

with:

val fingerprintManagerInstance = applicationContext.getSystemService(FINGERPRINT_SERVICE) ?: return

and see if you get better results.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
2

CommonsWare's answer resolves the first cause of the Activity memory leak, and was a great help in tracking down the second one.

The second cause is that FingerprintManager holds a strong reference to the callback object in FingerprintManager.mAuthenticationCallback and doesn't release it until a different callback object is provided by another authenticate() call.

This is a known issue which they have not fixed yet as of Dec 17, 2018.

My workaround (kludge) is to make another authenticate() call with an empty callback object that was created in the application context, and then immediately call onAuthenticationFailed() on the empty callback object.

Its messy and I would definitely vote up a better, more elegant solution.


Declare a static variable somewhere (in a class named App in this example) to hold the empty callback object.

public static FingerprintManager.AuthenticationCallback EmptyAuthenticationCallback;

Instantiate it in onCreate() of the application subclass if appropriate. Note that this requires API 23+, so make sure your app does not try to use it when in lower APIs.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    App.EmptyAuthenticationCallback = new FingerprintManager.AuthenticationCallback() {};
}

Within the FingerprintManager.AuthenticationCallback() anonymous object add a clearCallbackReference() method.

private void clearCallbackReference() {
    final String methodName = "clearCallbackReference()";
    // FingerprintManager holds a strong reference to the callback
    //   which in turn holds a strong reference to the Activity
    //   and thus causes the Activity to be leaked.
    // This is a known bug in the FingerprintManager class.
    //   http://code.google.com/p/android/issues/detail?id=215512
    // And the CancellationSignal object does not clear the callback reference either.
    //
    // To clear it we call authenticate() again and give it a new callback
    //   (created in the application context instead of the Activity context),
    //   and then immediately "fail" the authenticate() call
    //   since we aren't wanting another fingerprint from the user.
    try {
        Log.d(TAG, methodName);
        fingerprintManager.authenticate(null, null, 0, App.EmptyAuthenticationCallback, null);
        App.EmptyAuthenticationCallback.onAuthenticationFailed();
    }
    catch (Exception ex) {
        // Handle the exception..
    }
}

Revise your onAuthenticationSucceeded() & onAuthenticationError() methods in FingerprintManager.AuthenticationCallback() to call clearCallbackReference().

Example:

@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
    final String methodName = "onAuthenticationSucceeded()";
    try {
        Log.d(TAG, methodName + ": Authentication succeeded for Action '" + action + "'.");
        super.onAuthenticationSucceeded(result);
        // Do your custom actions here if needed.
    }
    catch (Exception ex) {
        // Handle the exception..
    }
    finally {
        clearCallbackReference();
    }
}

In onAuthenticationError() my finally block looks like this because sometimes errMsgId 5 "Fingerprint operation canceled." is a bogus error. It is commonly triggered right after the authenticate() call, but the operation it not really canceled.

finally {
    if (errMsgId != 5 || (canceler != null && canceler.isCanceled()))
        clearCallbackReference();
}

canceler is the CancellationSignal object, passed in as a param.

jk7
  • 1,958
  • 1
  • 22
  • 31
  • Glad to see you got it sorted. EmptyAuthenticationCallback doesn't do any logging currently because its a separate instance with no custom logic in it. That's why I chose to name it "Empty". You could add custom logic to it like you did for the one in your Activity. Just be sure your custom logic doesn't capture an Activity context and cause another leak. – jk7 Dec 21 '18 at 20:32
  • @Drocchio I guess it was a combination of a lot of things - research and testing. CommonsWare provided the first big clue. And I found [this explanation](https://github.com/wultra/powerauth-mobile-sdk/issues/129/) helpful. They are discussing roughly the same issue, but it seems to be within a wrapper framework called PowerAuth. Studying [their code fix](https://github.com/wultra/powerauth-mobile-sdk/commit/40c07ea6523be1652d8879ff364460b2224399dc) helpful, but not a solution I could use directly. However it led to the realization that callback is released by another auth call. – jk7 Dec 23 '18 at 19:34
  • @Drocchio and the bogus call to onAuthenticationError() was discovered through testing since it was causing things to not work right. – jk7 Dec 23 '18 at 19:36
  • thank you very much to share your process, it will be really useful for tricky future bugs to learn from your smart experience, and insightful intuition and yes, commonsWare is commonsWare:) I wish you a nice new year. – Drocchio Dec 26 '18 at 11:16
  • +1, thanks for the answer! I kinda figured out that I need to reassign the callback to null, but it is of NonNullable type in the api. Didn't come up with the solution of creating a new instance of the callback with the app context. It is totally a hacky solution but works. I wouldn't expect of Google to make such a bad mistake with this hard reference. – Rainmaker Jan 13 '19 at 23:15