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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user