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