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