/cc @mcomella
Settings > Logins and passwords > Saved logins > unlock
No leak.
β¬βββ
β GC Root: Global variable in native code
β
ββ dalvik.system.PathClassLoader instance
β Leaking: NO (InternalLeakCanaryβ is not leaking and A ClassLoader is never leaking)
β β PathClassLoader.runtimeInternalObjects
ββ java.lang.Object[] array
β Leaking: NO (InternalLeakCanaryβ is not leaking)
β β Object[].[891]
ββ leakcanary.internal.InternalLeakCanary class
β Leaking: NO (HomeActivityβ is not leaking and a class is never leaking)
β β static InternalLeakCanary.resumedActivity
ββ org.mozilla.fenix.HomeActivity instance
β Leaking: NO (Activity#mDestroyed is false)
β β HomeActivity.mViewModelStore
β ~~~~~~~~~~~~~~~
ββ androidx.lifecycle.ViewModelStore instance
β Leaking: UNKNOWN
β β ViewModelStore.mMap
β ~~~~
ββ java.util.HashMap instance
β Leaking: UNKNOWN
β β HashMap.table
β ~~~~~
ββ java.util.HashMap$Node[] array
β Leaking: UNKNOWN
β β HashMap$Node[].[2]
β ~~~
ββ java.util.HashMap$Node instance
β Leaking: UNKNOWN
β β HashMap$Node.value
β ~~~~~
ββ androidx.biometric.BiometricViewModel instance
β Leaking: UNKNOWN
β β BiometricViewModel.mClientCallback
β ~~~~~~~~~~~~~~~
ββ org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragment$biometricPromptCallback$1 instance
β Leaking: UNKNOWN
β Anonymous subclass of androidx.biometric.BiometricPrompt$AuthenticationCallback
β β SavedLoginsAuthFragment$biometricPromptCallback$1.this$0
β ~~~~~~
β°β org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragment instance
β Leaking: YES (ObjectWatcher was watching this because org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
β key = 77a534a9-73fb-4b7e-9bb1-1c26e0064ca8
β watchDurationMillis = 9964
β retainedDurationMillis = 4964
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: OnePlus
LeakCanary version: 2.4
App process name: org.mozilla.fenix.debug
Analysis duration: 6083 ms
Side note: I'd suggest using triple quotes when sharing traces (recommend admins edit the bug report).
HomeActivity is alive (not destroyed), it references its ViewModelStore which contains a BiometricViewModel. From that we can deduce that the lifecycle of BiometricViewModel is tied to the lifecycle of the activity. However BiometricViewModel has an mClientCallback field that references an anonymous class defined in SavedLoginsAuthFragment, and SavedLoginsAuthFragment is destroyed.
This likely means that SavedLoginsAuthFragment somehow registered a callback to BiometricViewModel but didn't clear it when destroyed.
The leaking object, biometricPromptCallback, an anonymous implemention of BiometricPrompt.AuthenticationCallback, is defined here
It's registered here:
biometricPrompt = BiometricPrompt(this, executor, biometricPromptCallback)
Looking at the Android X BiometricPrompt sources, the callback is set in BiometricPrompt.init() which is called from the BiometricPrompt constructor and never unset.
private void init(
@Nullable FragmentActivity activity,
@Nullable FragmentManager fragmentManager,
@Nullable Executor executor,
@NonNull AuthenticationCallback callback) {
mClientFragmentManager = fragmentManager;
if (activity != null) {
final BiometricViewModel viewModel =
new ViewModelProvider(activity).get(BiometricViewModel.class);
if (executor != null) {
viewModel.setClientExecutor(executor);
}
viewModel.setClientCallback(callback);
}
}
So, as it stands, there is no way to clear the callback, and the view model is always created from an activity, so the callback always has the lifecycle of the activity. This looks like a design issue with how Android X BiometricPrompt work. Until Google fixes it, the AuthenticationCallback implementation needs to stop being an anonymous class and it needs to have a nullable reference to the fragment.
Most helpful comment
Side note: I'd suggest using triple quotes when sharing traces (recommend admins edit the bug report).
HomeActivity is alive (not destroyed), it references its ViewModelStore which contains a BiometricViewModel. From that we can deduce that the lifecycle of BiometricViewModel is tied to the lifecycle of the activity. However BiometricViewModel has an mClientCallback field that references an anonymous class defined in SavedLoginsAuthFragment, and SavedLoginsAuthFragment is destroyed.
This likely means that SavedLoginsAuthFragment somehow registered a callback to BiometricViewModel but didn't clear it when destroyed.
The leaking object,
biometricPromptCallback, an anonymous implemention ofBiometricPrompt.AuthenticationCallback, is defined hereIt's registered here:
Looking at the Android X
BiometricPromptsources, the callback is set inBiometricPrompt.init()which is called from theBiometricPromptconstructor and never unset.So, as it stands, there is no way to clear the callback, and the view model is always created from an activity, so the callback always has the lifecycle of the activity. This looks like a design issue with how Android X BiometricPrompt work. Until Google fixes it, the AuthenticationCallback implementation needs to stop being an anonymous class and it needs to have a nullable reference to the fragment.