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