1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.applications.autofill; 18 19 import static android.app.admin.DevicePolicyResources.Strings.Settings.AUTO_SYNC_PERSONAL_DATA; 20 import static android.app.admin.DevicePolicyResources.Strings.Settings.AUTO_SYNC_PRIVATE_DATA; 21 import static android.app.admin.DevicePolicyResources.Strings.Settings.AUTO_SYNC_WORK_DATA; 22 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 23 import static android.service.autofill.AutofillService.EXTRA_RESULT; 24 25 import static androidx.lifecycle.Lifecycle.Event.ON_CREATE; 26 import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY; 27 28 import android.annotation.UserIdInt; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.ServiceConnection; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ServiceInfo; 35 import android.graphics.drawable.Drawable; 36 import android.os.Bundle; 37 import android.os.IBinder; 38 import android.os.RemoteException; 39 import android.os.UserHandle; 40 import android.service.autofill.AutofillService; 41 import android.service.autofill.AutofillServiceInfo; 42 import android.service.autofill.IAutoFillService; 43 import android.text.TextUtils; 44 import android.util.IconDrawableFactory; 45 import android.util.Log; 46 47 import androidx.lifecycle.LifecycleObserver; 48 import androidx.lifecycle.LifecycleOwner; 49 import androidx.lifecycle.MutableLiveData; 50 import androidx.lifecycle.OnLifecycleEvent; 51 import androidx.preference.PreferenceGroup; 52 import androidx.preference.PreferenceScreen; 53 54 import com.android.internal.annotations.VisibleForTesting; 55 import com.android.internal.os.IResultReceiver; 56 import com.android.settings.R; 57 import com.android.settings.Utils; 58 import com.android.settings.core.BasePreferenceController; 59 import com.android.settingslib.utils.StringUtil; 60 import com.android.settingslib.widget.AppPreference; 61 62 import java.lang.ref.WeakReference; 63 import java.util.ArrayList; 64 import java.util.List; 65 import java.util.concurrent.atomic.AtomicBoolean; 66 67 /** 68 * Queries available autofill services and adds preferences for those that declare passwords 69 * settings. 70 * <p> 71 * The controller binds to each service to fetch the number of saved passwords in each. 72 */ 73 public class PasswordsPreferenceController extends BasePreferenceController 74 implements LifecycleObserver { 75 private static final String TAG = "AutofillSettings"; 76 private static final boolean DEBUG = false; 77 78 private final PackageManager mPm; 79 private final IconDrawableFactory mIconFactory; 80 private final List<AutofillServiceInfo> mServices; 81 82 private LifecycleOwner mLifecycleOwner; 83 PasswordsPreferenceController(Context context, String preferenceKey)84 public PasswordsPreferenceController(Context context, String preferenceKey) { 85 super(context, preferenceKey); 86 mPm = context.getPackageManager(); 87 mIconFactory = IconDrawableFactory.newInstance(mContext); 88 mServices = new ArrayList<>(); 89 } 90 91 @OnLifecycleEvent(ON_CREATE) onCreate(LifecycleOwner lifecycleOwner)92 void onCreate(LifecycleOwner lifecycleOwner) { 93 init(lifecycleOwner, AutofillServiceInfo.getAvailableServices(mContext, getUser())); 94 } 95 96 @VisibleForTesting init(LifecycleOwner lifecycleOwner, List<AutofillServiceInfo> availableServices)97 void init(LifecycleOwner lifecycleOwner, List<AutofillServiceInfo> availableServices) { 98 mLifecycleOwner = lifecycleOwner; 99 100 for (int i = availableServices.size() - 1; i >= 0; i--) { 101 final String passwordsActivity = availableServices.get(i).getPasswordsActivity(); 102 if (TextUtils.isEmpty(passwordsActivity)) { 103 availableServices.remove(i); 104 } 105 } 106 // TODO: Reverse the loop above and add to mServices directly. 107 mServices.clear(); 108 mServices.addAll(availableServices); 109 } 110 111 @Override getAvailabilityStatus()112 public int getAvailabilityStatus() { 113 return mServices.isEmpty() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; 114 } 115 116 @Override displayPreference(PreferenceScreen screen)117 public void displayPreference(PreferenceScreen screen) { 118 super.displayPreference(screen); 119 final PreferenceGroup group = screen.findPreference(getPreferenceKey()); 120 addPasswordPreferences(screen.getContext(), getUser(), group); 121 122 replaceEnterpriseStringTitle(screen, "auto_sync_personal_account_data", 123 AUTO_SYNC_PERSONAL_DATA, R.string.account_settings_menu_auto_sync_personal); 124 replaceEnterpriseStringTitle(screen, "auto_sync_work_account_data", 125 AUTO_SYNC_WORK_DATA, R.string.account_settings_menu_auto_sync_work); 126 replaceEnterpriseStringTitle(screen, "auto_sync_private_account_data", 127 AUTO_SYNC_PRIVATE_DATA, R.string.account_settings_menu_auto_sync_private); 128 } 129 addPasswordPreferences( Context prefContext, @UserIdInt int user, PreferenceGroup group)130 private void addPasswordPreferences( 131 Context prefContext, @UserIdInt int user, PreferenceGroup group) { 132 for (int i = 0; i < mServices.size(); i++) { 133 final AutofillServiceInfo service = mServices.get(i); 134 final AppPreference pref = new AppPreference(prefContext); 135 final ServiceInfo serviceInfo = service.getServiceInfo(); 136 pref.setTitle(serviceInfo.loadLabel(mPm)); 137 final Drawable icon = 138 mIconFactory.getBadgedIcon( 139 serviceInfo, 140 serviceInfo.applicationInfo, 141 user); 142 pref.setIcon(Utils.getSafeIcon(icon)); 143 pref.setOnPreferenceClickListener(p -> { 144 final Intent intent = 145 new Intent(Intent.ACTION_MAIN) 146 .setClassName( 147 serviceInfo.packageName, 148 service.getPasswordsActivity()) 149 .setFlags(FLAG_ACTIVITY_NEW_TASK); 150 prefContext.startActivityAsUser(intent, UserHandle.of(user)); 151 return true; 152 }); 153 // Set a placeholder summary to avoid a UI flicker when the value loads. 154 pref.setSummary(R.string.autofill_passwords_count_placeholder); 155 156 final MutableLiveData<Integer> passwordCount = new MutableLiveData<>(); 157 passwordCount.observe( 158 mLifecycleOwner, count -> { 159 // TODO(b/169455298): Validate the result. 160 final CharSequence summary = StringUtil.getIcuPluralsString(mContext, count, 161 R.string.autofill_passwords_count); 162 pref.setSummary(summary); 163 }); 164 // TODO(b/169455298): Limit the number of concurrent queries. 165 // TODO(b/169455298): Cache the results for some time. 166 requestSavedPasswordCount(service, user, passwordCount); 167 168 group.addPreference(pref); 169 } 170 } 171 requestSavedPasswordCount( AutofillServiceInfo service, @UserIdInt int user, MutableLiveData<Integer> data)172 private void requestSavedPasswordCount( 173 AutofillServiceInfo service, @UserIdInt int user, MutableLiveData<Integer> data) { 174 final Intent intent = 175 new Intent(AutofillService.SERVICE_INTERFACE) 176 .setComponent(service.getServiceInfo().getComponentName()); 177 final AutofillServiceConnection connection = new AutofillServiceConnection(mContext, data); 178 if (mContext.bindServiceAsUser( 179 intent, connection, Context.BIND_AUTO_CREATE, UserHandle.of(user))) { 180 connection.mBound.set(true); 181 mLifecycleOwner.getLifecycle().addObserver(connection); 182 } 183 } 184 185 private static class AutofillServiceConnection implements ServiceConnection, LifecycleObserver { 186 final WeakReference<Context> mContext; 187 final MutableLiveData<Integer> mData; 188 final AtomicBoolean mBound = new AtomicBoolean(); 189 AutofillServiceConnection(Context context, MutableLiveData<Integer> data)190 AutofillServiceConnection(Context context, MutableLiveData<Integer> data) { 191 mContext = new WeakReference<>(context); 192 mData = data; 193 } 194 195 @Override onServiceConnected(ComponentName name, IBinder service)196 public void onServiceConnected(ComponentName name, IBinder service) { 197 final IAutoFillService autofillService = IAutoFillService.Stub.asInterface(service); 198 if (DEBUG) { 199 Log.d(TAG, "Fetching password count from " + name); 200 } 201 try { 202 autofillService.onSavedPasswordCountRequest( 203 new IResultReceiver.Stub() { 204 @Override 205 public void send(int resultCode, Bundle resultData) { 206 if (DEBUG) { 207 Log.d(TAG, "Received password count result " + resultCode 208 + " from " + name); 209 } 210 if (resultCode == 0 && resultData != null) { 211 mData.postValue(resultData.getInt(EXTRA_RESULT)); 212 } 213 unbind(); 214 } 215 }); 216 } catch (RemoteException e) { 217 Log.e(TAG, "Failed to fetch password count: " + e); 218 } 219 } 220 221 @Override onServiceDisconnected(ComponentName name)222 public void onServiceDisconnected(ComponentName name) { 223 } 224 225 @OnLifecycleEvent(ON_DESTROY) unbind()226 void unbind() { 227 if (!mBound.getAndSet(false)) { 228 return; 229 } 230 final Context context = mContext.get(); 231 if (context != null) { 232 context.unbindService(this); 233 } 234 } 235 } 236 getUser()237 private int getUser() { 238 UserHandle workUser = getWorkProfileUser(); 239 return workUser != null ? workUser.getIdentifier() : UserHandle.myUserId(); 240 } 241 } 242