Loading src/com/android/settings/applications/autofill/PasswordsPreferenceController.java +104 −1 Original line number Diff line number Diff line Loading @@ -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. * <p> * 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<AutofillServiceInfo> mServices; private LifecycleOwner mLifecycleOwner; public PasswordsPreferenceController(Context context, String preferenceKey) { this(context, preferenceKey, AutofillServiceInfo.getAvailableServices(context, UserHandle.myUserId())); Loading @@ -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; Loading Loading @@ -96,7 +127,79 @@ public class PasswordsPreferenceController extends BasePreferenceController { pref.setIntent( new Intent(Intent.ACTION_MAIN) .setClassName(serviceInfo.packageName, service.getPasswordsActivity())); final MutableLiveData<Integer> 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<Integer> 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<Context> mContext; final MutableLiveData<Integer> mData; final AtomicBoolean mBound = new AtomicBoolean(); AutofillServiceConnection(Context context, MutableLiveData<Integer> 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); } } } } tests/unit/src/com/android/settings/applications/autofill/PasswordsPreferenceControllerTest.java +9 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading Loading
src/com/android/settings/applications/autofill/PasswordsPreferenceController.java +104 −1 Original line number Diff line number Diff line Loading @@ -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. * <p> * 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<AutofillServiceInfo> mServices; private LifecycleOwner mLifecycleOwner; public PasswordsPreferenceController(Context context, String preferenceKey) { this(context, preferenceKey, AutofillServiceInfo.getAvailableServices(context, UserHandle.myUserId())); Loading @@ -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; Loading Loading @@ -96,7 +127,79 @@ public class PasswordsPreferenceController extends BasePreferenceController { pref.setIntent( new Intent(Intent.ACTION_MAIN) .setClassName(serviceInfo.packageName, service.getPasswordsActivity())); final MutableLiveData<Integer> 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<Integer> 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<Context> mContext; final MutableLiveData<Integer> mData; final AtomicBoolean mBound = new AtomicBoolean(); AutofillServiceConnection(Context context, MutableLiveData<Integer> 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); } } } }
tests/unit/src/com/android/settings/applications/autofill/PasswordsPreferenceControllerTest.java +9 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading