Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 63fb2f10 authored by Ahaan Ugale's avatar Ahaan Ugale
Browse files

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
parent 2160d5fc
Loading
Loading
Loading
Loading
+104 −1
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -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);