/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.autofill;
import static android.app.admin.DevicePolicyResources.Strings.Settings.AUTO_SYNC_PERSONAL_DATA;
import static android.app.admin.DevicePolicyResources.Strings.Settings.AUTO_SYNC_PRIVATE_DATA;
import static android.app.admin.DevicePolicyResources.Strings.Settings.AUTO_SYNC_WORK_DATA;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
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.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.AppPreference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
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
implements LifecycleObserver {
private static final String TAG = "AutofillSettings";
private static final boolean DEBUG = false;
private final PackageManager mPm;
private final IconDrawableFactory mIconFactory;
private final List mServices;
private LifecycleOwner mLifecycleOwner;
public PasswordsPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mPm = context.getPackageManager();
mIconFactory = IconDrawableFactory.newInstance(mContext);
mServices = new ArrayList<>();
}
@OnLifecycleEvent(ON_CREATE)
void onCreate(LifecycleOwner lifecycleOwner) {
init(lifecycleOwner, AutofillServiceInfo.getAvailableServices(mContext, getUser()));
}
@VisibleForTesting
void init(LifecycleOwner lifecycleOwner, List availableServices) {
mLifecycleOwner = lifecycleOwner;
for (int i = availableServices.size() - 1; i >= 0; i--) {
final String passwordsActivity = availableServices.get(i).getPasswordsActivity();
if (TextUtils.isEmpty(passwordsActivity)) {
availableServices.remove(i);
}
}
// TODO: Reverse the loop above and add to mServices directly.
mServices.clear();
mServices.addAll(availableServices);
}
@Override
public int getAvailabilityStatus() {
return mServices.isEmpty() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
final PreferenceGroup group = screen.findPreference(getPreferenceKey());
addPasswordPreferences(screen.getContext(), getUser(), group);
replaceEnterpriseStringTitle(screen, "auto_sync_personal_account_data",
AUTO_SYNC_PERSONAL_DATA, R.string.account_settings_menu_auto_sync_personal);
replaceEnterpriseStringTitle(screen, "auto_sync_work_account_data",
AUTO_SYNC_WORK_DATA, R.string.account_settings_menu_auto_sync_work);
replaceEnterpriseStringTitle(screen, "auto_sync_private_account_data",
AUTO_SYNC_PRIVATE_DATA, R.string.account_settings_menu_auto_sync_private);
}
private void addPasswordPreferences(
Context prefContext, @UserIdInt int user, PreferenceGroup group) {
for (int i = 0; i < mServices.size(); i++) {
final AutofillServiceInfo service = mServices.get(i);
final AppPreference pref = new AppPreference(prefContext);
final ServiceInfo serviceInfo = service.getServiceInfo();
pref.setTitle(serviceInfo.loadLabel(mPm));
final Drawable icon =
mIconFactory.getBadgedIcon(
serviceInfo,
serviceInfo.applicationInfo,
user);
pref.setIcon(Utils.getSafeIcon(icon));
pref.setOnPreferenceClickListener(p -> {
final Intent intent =
new Intent(Intent.ACTION_MAIN)
.setClassName(
serviceInfo.packageName,
service.getPasswordsActivity())
.setFlags(FLAG_ACTIVITY_NEW_TASK);
prefContext.startActivityAsUser(intent, UserHandle.of(user));
return true;
});
// Set a placeholder summary to avoid a UI flicker when the value loads.
pref.setSummary(R.string.autofill_passwords_count_placeholder);
final MutableLiveData passwordCount = new MutableLiveData<>();
passwordCount.observe(
mLifecycleOwner, count -> {
// TODO(b/169455298): Validate the result.
final CharSequence summary = StringUtil.getIcuPluralsString(mContext, count,
R.string.autofill_passwords_count);
pref.setSummary(summary);
});
// 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);
if (DEBUG) {
Log.d(TAG, "Fetching password count from " + name);
}
try {
autofillService.onSavedPasswordCountRequest(
new IResultReceiver.Stub() {
@Override
public void send(int resultCode, Bundle resultData) {
if (DEBUG) {
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);
}
}
}
private int getUser() {
UserHandle workUser = getWorkProfileUser();
return workUser != null ? workUser.getIdentifier() : UserHandle.myUserId();
}
}