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