• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.car.settings.accounts;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.app.Activity;
22 import android.car.drivingstate.CarUxRestrictions;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentSender;
27 import android.content.SyncAdapterType;
28 import android.content.SyncInfo;
29 import android.content.SyncStatusInfo;
30 import android.content.SyncStatusObserver;
31 import android.content.pm.PackageManager;
32 import android.os.UserHandle;
33 import android.text.format.DateFormat;
34 
35 import androidx.annotation.Nullable;
36 import androidx.annotation.VisibleForTesting;
37 import androidx.collection.ArrayMap;
38 import androidx.preference.Preference;
39 import androidx.preference.PreferenceGroup;
40 
41 import com.android.car.settings.R;
42 import com.android.car.settings.common.FragmentController;
43 import com.android.car.settings.common.Logger;
44 import com.android.car.settings.common.PreferenceController;
45 import com.android.settingslib.accounts.AuthenticatorHelper;
46 import com.android.settingslib.utils.ThreadUtils;
47 
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.Comparator;
51 import java.util.Date;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 
57 /**
58  * Controller that presents all visible sync adapters for an account.
59  *
60  * <p>Largely derived from {@link com.android.settings.accounts.AccountSyncSettings}.
61  */
62 public class AccountSyncDetailsPreferenceController extends
63         PreferenceController<PreferenceGroup> implements
64         AuthenticatorHelper.OnAccountsUpdateListener {
65     private static final Logger LOG = new Logger(AccountSyncDetailsPreferenceController.class);
66     /**
67      * Preferences are keyed by authority so that existing SyncPreferences can be reused on account
68      * sync.
69      */
70     private final Map<String, SyncPreference> mSyncPreferences = new ArrayMap<>();
71     private Account mAccount;
72     private UserHandle mUserHandle;
73     private AuthenticatorHelper mAuthenticatorHelper;
74     private Object mStatusChangeListenerHandle;
75     private SyncStatusObserver mSyncStatusObserver =
76             which -> ThreadUtils.postOnMainThread(() -> {
77                 // The observer call may occur even if the fragment hasn't been started, so
78                 // only force an update if the fragment hasn't been stopped.
79                 if (isStarted()) {
80                     forceUpdateSyncCategory();
81                 }
82             });
83 
AccountSyncDetailsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)84     public AccountSyncDetailsPreferenceController(Context context, String preferenceKey,
85             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
86         super(context, preferenceKey, fragmentController, uxRestrictions);
87     }
88 
89     /** Sets the account that the sync preferences are being shown for. */
setAccount(Account account)90     public void setAccount(Account account) {
91         mAccount = account;
92     }
93 
94     /** Sets the user handle used by the controller. */
setUserHandle(UserHandle userHandle)95     public void setUserHandle(UserHandle userHandle) {
96         mUserHandle = userHandle;
97     }
98 
99     @Override
getPreferenceType()100     protected Class<PreferenceGroup> getPreferenceType() {
101         return PreferenceGroup.class;
102     }
103 
104     /**
105      * Verifies that the controller was properly initialized with {@link #setAccount(Account)} and
106      * {@link #setUserHandle(UserHandle)}.
107      *
108      * @throws IllegalStateException if the account or user handle is {@code null}
109      */
110     @Override
checkInitialized()111     protected void checkInitialized() {
112         LOG.v("checkInitialized");
113         if (mAccount == null) {
114             throw new IllegalStateException(
115                     "AccountSyncDetailsPreferenceController must be initialized by calling "
116                             + "setAccount(Account)");
117         }
118         if (mUserHandle == null) {
119             throw new IllegalStateException(
120                     "AccountSyncDetailsPreferenceController must be initialized by calling "
121                             + "setUserHandle(UserHandle)");
122         }
123     }
124 
125     /**
126      * Initializes the authenticator helper.
127      */
128     @Override
onCreateInternal()129     protected void onCreateInternal() {
130         mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserHandle, /* listener= */
131                 this);
132     }
133 
134     /**
135      * Registers the account update and sync status change callbacks.
136      */
137     @Override
onStartInternal()138     protected void onStartInternal() {
139         mAuthenticatorHelper.listenToAccountUpdates();
140 
141         mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener(
142                 ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
143                         | ContentResolver.SYNC_OBSERVER_TYPE_STATUS
144                         | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
145     }
146 
147     /**
148      * Unregisters the account update and sync status change callbacks.
149      */
150     @Override
onStopInternal()151     protected void onStopInternal() {
152         mAuthenticatorHelper.stopListeningToAccountUpdates();
153         if (mStatusChangeListenerHandle != null) {
154             ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle);
155         }
156     }
157 
158     @Override
onAccountsUpdate(UserHandle userHandle)159     public void onAccountsUpdate(UserHandle userHandle) {
160         // Only force a refresh if accounts have changed for the current user.
161         if (userHandle.equals(mUserHandle)) {
162             forceUpdateSyncCategory();
163         }
164     }
165 
166     @Override
updateState(PreferenceGroup preferenceGroup)167     public void updateState(PreferenceGroup preferenceGroup) {
168         // Add preferences for each account if the controller should be available
169         forceUpdateSyncCategory();
170     }
171 
172     /**
173      * Handles toggling/syncing when a sync preference is clicked on.
174      *
175      * <p>Largely derived from
176      * {@link com.android.settings.accounts.AccountSyncSettings#onPreferenceTreeClick}.
177      */
onSyncPreferenceClicked(SyncPreference preference)178     private boolean onSyncPreferenceClicked(SyncPreference preference) {
179         String authority = preference.getKey();
180         String packageName = preference.getPackageName();
181         int uid = preference.getUid();
182         if (preference.isOneTimeSyncMode()) {
183             // If the sync adapter doesn't have access to the account we either
184             // request access by starting an activity if possible or kick off the
185             // sync which will end up posting an access request notification.
186             if (requestAccountAccessIfNeeded(packageName, uid)) {
187                 return true;
188             }
189             requestSync(authority);
190         } else {
191             boolean syncOn = preference.isChecked();
192             int userId = mUserHandle.getIdentifier();
193             boolean oldSyncState = ContentResolver.getSyncAutomaticallyAsUser(mAccount,
194                     authority, userId);
195             if (syncOn != oldSyncState) {
196                 // Toggling this switch triggers sync but we may need a user approval. If the
197                 // sync adapter doesn't have access to the account we either request access by
198                 // starting an activity if possible or kick off the sync which will end up
199                 // posting an access request notification.
200                 if (syncOn && requestAccountAccessIfNeeded(packageName, uid)) {
201                     return true;
202                 }
203                 // If we're enabling sync, this will request a sync as well.
204                 ContentResolver.setSyncAutomaticallyAsUser(mAccount, authority, syncOn, userId);
205                 if (syncOn) {
206                     requestSync(authority);
207                 } else {
208                     cancelSync(authority);
209                 }
210             }
211         }
212         return true;
213     }
214 
requestSync(String authority)215     private void requestSync(String authority) {
216         AccountSyncHelper.requestSyncIfAllowed(mAccount, authority, mUserHandle.getIdentifier());
217     }
218 
cancelSync(String authority)219     private void cancelSync(String authority) {
220         ContentResolver.cancelSyncAsUser(mAccount, authority, mUserHandle.getIdentifier());
221     }
222 
223     /**
224      * Requests account access if needed.
225      *
226      * <p>Copied from
227      * {@link com.android.settings.accounts.AccountSyncSettings#requestAccountAccessIfNeeded}.
228      */
requestAccountAccessIfNeeded(String packageName, int uid)229     private boolean requestAccountAccessIfNeeded(String packageName, int uid) {
230         if (packageName == null) {
231             return false;
232         }
233 
234         AccountManager accountManager = getContext().getSystemService(AccountManager.class);
235         if (!accountManager.hasAccountAccess(mAccount, packageName, mUserHandle)) {
236             IntentSender intent = accountManager.createRequestAccountAccessIntentSenderAsUser(
237                     mAccount, packageName, mUserHandle);
238             if (intent != null) {
239                 try {
240                     getFragmentController().startIntentSenderForResult(intent,
241                             uid, /* fillInIntent= */ null, /* flagsMask= */ 0,
242                             /* flagsValues= */ 0, /* options= */ null,
243                             this::onAccountRequestApproved);
244                     return true;
245                 } catch (IntentSender.SendIntentException e) {
246                     LOG.e("Error requesting account access", e);
247                 }
248             }
249         }
250         return false;
251     }
252 
253     /** Handles a sync adapter refresh when an account request was approved. */
onAccountRequestApproved(int uid, int resultCode, @Nullable Intent data)254     public void onAccountRequestApproved(int uid, int resultCode, @Nullable Intent data) {
255         if (resultCode == Activity.RESULT_OK) {
256             for (SyncPreference pref : mSyncPreferences.values()) {
257                 if (pref.getUid() == uid) {
258                     onSyncPreferenceClicked(pref);
259                     return;
260                 }
261             }
262         }
263     }
264 
265     /** Forces a refresh of the sync adapter preferences. */
forceUpdateSyncCategory()266     private void forceUpdateSyncCategory() {
267         Set<String> preferencesToRemove = new HashSet<>(mSyncPreferences.keySet());
268         List<SyncPreference> preferences = getSyncPreferences(preferencesToRemove);
269 
270         // Sort the preferences, add the ones that need to be added, and remove the ones that need
271         // to be removed. Manually set the order so that existing preferences are reordered
272         // correctly.
273         Collections.sort(preferences, Comparator.comparing(
274                 (SyncPreference a) -> a.getTitle().toString())
275                 .thenComparing((SyncPreference a) -> a.getSummary().toString()));
276 
277         for (int i = 0; i < preferences.size(); i++) {
278             SyncPreference pref = preferences.get(i);
279             pref.setOrder(i);
280             mSyncPreferences.put(pref.getKey(), pref);
281             getPreference().addPreference(pref);
282         }
283 
284         for (String key : preferencesToRemove) {
285             getPreference().removePreference(mSyncPreferences.get(key));
286             mSyncPreferences.remove(key);
287         }
288     }
289 
290     /**
291      * Returns a list of preferences corresponding to the visible sync adapters for the current
292      * user.
293      *
294      * <p> Derived from {@link com.android.settings.accounts.AccountSyncSettings#setFeedsState}
295      * and {@link com.android.settings.accounts.AccountSyncSettings#updateAccountSwitches}.
296      *
297      * @param preferencesToRemove the keys for the preferences currently being shown; only the keys
298      *                            for preferences to be removed will remain after method execution
299      */
getSyncPreferences(Set<String> preferencesToRemove)300     private List<SyncPreference> getSyncPreferences(Set<String> preferencesToRemove) {
301         int userId = mUserHandle.getIdentifier();
302         PackageManager packageManager = getContext().getPackageManager();
303         List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
304         // Whether one time sync is enabled rather than automtic sync
305         boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser(userId);
306 
307         List<SyncPreference> syncPreferences = new ArrayList<>();
308 
309         Set<SyncAdapterType> syncAdapters = AccountSyncHelper.getVisibleSyncAdaptersForAccount(
310                 getContext(), mAccount, mUserHandle);
311         for (SyncAdapterType syncAdapter : syncAdapters) {
312             String authority = syncAdapter.authority;
313 
314             int uid;
315             try {
316                 uid = packageManager.getPackageUidAsUser(syncAdapter.getPackageName(), userId);
317             } catch (PackageManager.NameNotFoundException e) {
318                 LOG.e("No uid for package" + syncAdapter.getPackageName(), e);
319                 // If we can't get the Uid for the package hosting the sync adapter, don't show it
320                 continue;
321             }
322 
323             // If we've reached this point, the sync adapter should be shown. If a preference for
324             // the sync adapter already exists, update its state. Otherwise, create a new
325             // preference.
326             SyncPreference pref = mSyncPreferences.getOrDefault(authority,
327                     new SyncPreference(getContext(), authority));
328             pref.setUid(uid);
329             pref.setPackageName(syncAdapter.getPackageName());
330             pref.setOnPreferenceClickListener(
331                     (Preference p) -> onSyncPreferenceClicked((SyncPreference) p));
332 
333             CharSequence title = AccountSyncHelper.getTitle(getContext(), authority, mUserHandle);
334             pref.setTitle(title);
335 
336             // Keep track of preferences that need to be added and removed
337             syncPreferences.add(pref);
338             preferencesToRemove.remove(authority);
339 
340             SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(mAccount, authority,
341                     userId);
342             boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(mAccount, authority,
343                     userId);
344             boolean activelySyncing = AccountSyncHelper.isSyncing(mAccount, currentSyncs,
345                     authority);
346 
347             // The preference should be checked if one one-time sync or regular sync is enabled
348             boolean checked = oneTimeSyncMode || syncEnabled;
349             pref.setChecked(checked);
350 
351             String summary = getSummary(status, syncEnabled, activelySyncing);
352             pref.setSummary(summary);
353 
354             // Update the sync state so the icon is updated
355             AccountSyncHelper.SyncState syncState = AccountSyncHelper.getSyncState(status,
356                     syncEnabled, activelySyncing);
357             pref.setSyncState(syncState);
358             pref.setOneTimeSyncMode(oneTimeSyncMode);
359         }
360 
361         return syncPreferences;
362     }
363 
getSummary(SyncStatusInfo status, boolean syncEnabled, boolean activelySyncing)364     private String getSummary(SyncStatusInfo status, boolean syncEnabled, boolean activelySyncing) {
365         long successEndTime = (status == null) ? 0 : status.lastSuccessTime;
366         // Set the summary based on the current syncing state
367         if (!syncEnabled) {
368             return getContext().getString(R.string.sync_disabled);
369         } else if (activelySyncing) {
370             return getContext().getString(R.string.sync_in_progress);
371         } else if (successEndTime != 0) {
372             Date date = new Date();
373             date.setTime(successEndTime);
374             String timeString = formatSyncDate(date);
375             return getContext().getString(R.string.last_synced, timeString);
376         }
377         return "";
378     }
379 
380     @VisibleForTesting
formatSyncDate(Date date)381     String formatSyncDate(Date date) {
382         return DateFormat.getDateFormat(getContext()).format(date) + " " + DateFormat.getTimeFormat(
383                 getContext()).format(date);
384     }
385 }
386