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