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