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