From 63fb2f10d57faa018d60bf4d674b92ada0eef799 Mon Sep 17 00:00:00 2001 From: Ahaan Ugale Date: Sun, 7 Mar 2021 10:47:17 -0800 Subject: [PATCH] Autofill Settings: Display the number of saved passwords. We bind to each available AutofillService, fetch the number of saved passwords, then unbind and update the UI. Each ServiceConnection is wired up to the controller's lifecycle so they can be unbound when the lifecycle owner is destroyed. Bug: 169455298 Test: manual - check value in the UI Test: manual - no ServiceConnection leak, even when there's no response Test: atest \ SettingsUnitTests:com.android.settings.applications.autofill.PasswordsPreferenceControllerTest Change-Id: I7008e979e9292b99c8611010e49b3e738c82bfed --- .../PasswordsPreferenceController.java | 105 +++++++++++++++++- .../PasswordsPreferenceControllerTest.java | 9 ++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java b/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java index f27530e0aea..123addac7e2 100644 --- a/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java +++ b/src/com/android/settings/applications/autofill/PasswordsPreferenceController.java @@ -16,37 +16,63 @@ package com.android.settings.applications.autofill; +import static android.service.autofill.AutofillService.EXTRA_RESULT; + +import static androidx.lifecycle.Lifecycle.Event.ON_CREATE; +import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY; + import android.annotation.UserIdInt; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandle; +import android.service.autofill.AutofillService; import android.service.autofill.AutofillServiceInfo; +import android.service.autofill.IAutoFillService; import android.text.TextUtils; import android.util.IconDrawableFactory; +import android.util.Log; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.OnLifecycleEvent; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.IResultReceiver; import com.android.settings.Utils; import com.android.settings.core.BasePreferenceController; +import java.lang.ref.WeakReference; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; /** * Queries available autofill services and adds preferences for those that declare passwords * settings. + *

+ * The controller binds to each service to fetch the number of saved passwords in each. */ -public class PasswordsPreferenceController extends BasePreferenceController { +public class PasswordsPreferenceController extends BasePreferenceController + implements LifecycleObserver { + private static final String TAG = "AutofillSettings"; private final PackageManager mPm; private final IconDrawableFactory mIconFactory; private final List mServices; + private LifecycleOwner mLifecycleOwner; + public PasswordsPreferenceController(Context context, String preferenceKey) { this(context, preferenceKey, AutofillServiceInfo.getAvailableServices(context, UserHandle.myUserId())); @@ -67,6 +93,11 @@ public class PasswordsPreferenceController extends BasePreferenceController { mServices = availableServices; } + @OnLifecycleEvent(ON_CREATE) + void onCreate(LifecycleOwner lifecycleOwner) { + mLifecycleOwner = lifecycleOwner; + } + @Override public int getAvailabilityStatus() { return mServices.isEmpty() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; @@ -96,7 +127,79 @@ public class PasswordsPreferenceController extends BasePreferenceController { pref.setIntent( new Intent(Intent.ACTION_MAIN) .setClassName(serviceInfo.packageName, service.getPasswordsActivity())); + + final MutableLiveData passwordCount = new MutableLiveData<>(); + passwordCount.observe( + // TODO(b/169455298): Validate the result. + // TODO(b/169455298): Use a Quantity String resource. + mLifecycleOwner, count -> pref.setSummary("" + count + " passwords saved")); + // TODO(b/169455298): Limit the number of concurrent queries. + // TODO(b/169455298): Cache the results for some time. + requestSavedPasswordCount(service, user, passwordCount); + group.addPreference(pref); } } + + private void requestSavedPasswordCount( + AutofillServiceInfo service, @UserIdInt int user, MutableLiveData data) { + final Intent intent = + new Intent(AutofillService.SERVICE_INTERFACE) + .setComponent(service.getServiceInfo().getComponentName()); + final AutofillServiceConnection connection = new AutofillServiceConnection(mContext, data); + if (mContext.bindServiceAsUser( + intent, connection, Context.BIND_AUTO_CREATE, UserHandle.of(user))) { + connection.mBound.set(true); + mLifecycleOwner.getLifecycle().addObserver(connection); + } + } + + private static class AutofillServiceConnection implements ServiceConnection, LifecycleObserver { + final WeakReference mContext; + final MutableLiveData mData; + final AtomicBoolean mBound = new AtomicBoolean(); + + AutofillServiceConnection(Context context, MutableLiveData data) { + mContext = new WeakReference<>(context); + mData = data; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + final IAutoFillService autofillService = IAutoFillService.Stub.asInterface(service); + // TODO check if debug is logged on user build. + Log.d(TAG, "Fetching password count from " + name); + try { + autofillService.onSavedPasswordCountRequest( + new IResultReceiver.Stub() { + @Override + public void send(int resultCode, Bundle resultData) { + Log.d(TAG, "Received password count result " + resultCode + + " from " + name); + if (resultCode == 0 && resultData != null) { + mData.postValue(resultData.getInt(EXTRA_RESULT)); + } + unbind(); + } + }); + } catch (RemoteException e) { + Log.e(TAG, "Failed to fetch password count: " + e); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + } + + @OnLifecycleEvent(ON_DESTROY) + void unbind() { + if (!mBound.getAndSet(false)) { + return; + } + final Context context = mContext.get(); + if (context != null) { + context.unbindService(this); + } + } + } } diff --git a/tests/unit/src/com/android/settings/applications/autofill/PasswordsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/autofill/PasswordsPreferenceControllerTest.java index 216658fcb12..25d989385d3 100644 --- a/tests/unit/src/com/android/settings/applications/autofill/PasswordsPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/applications/autofill/PasswordsPreferenceControllerTest.java @@ -21,21 +21,26 @@ import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_U import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + import android.content.ComponentName; import android.content.Context; import android.os.Looper; import android.service.autofill.AutofillServiceInfo; +import androidx.lifecycle.Lifecycle; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; +import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.collect.Lists; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -100,11 +105,15 @@ public class PasswordsPreferenceControllerTest { assertThat(mPasswordsPreferenceCategory.getPreferenceCount()).isEqualTo(0); } + @Ignore("TODO: Fix the test to handle the service binding.") @Test + @UiThreadTest public void displayPreference_withPasswords_addsPreference() { AutofillServiceInfo service = createServiceWithPasswords(); PasswordsPreferenceController controller = createControllerWithServices(Lists.newArrayList(service)); + controller.onCreate(() -> mock(Lifecycle.class)); + controller.displayPreference(mScreen); assertThat(mPasswordsPreferenceCategory.getPreferenceCount()).isEqualTo(1);