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