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
This commit is contained in:
Ahaan Ugale
2021-03-07 10:47:17 -08:00
parent 2160d5fc4c
commit 63fb2f10d5
2 changed files with 113 additions and 1 deletions

View File

@@ -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()));
@@ -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<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);
}
}
}
}

View File

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