Search code examples
androidkotlinmemory-leaksandroid-fingerprint-apileakcanary

Memory Leak, but how can I pass a different context than the one of the activity to solve the Leak?


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


Solution

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