1 /* 2 * Copyright (C) 2008 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.accounts; 18 19 import android.accounts.Account; 20 import android.accounts.AccountManager; 21 import android.app.Activity; 22 import android.app.Dialog; 23 import android.app.settings.SettingsEnums; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentSender; 28 import android.content.SyncAdapterType; 29 import android.content.SyncInfo; 30 import android.content.SyncStatusInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ProviderInfo; 33 import android.content.pm.UserInfo; 34 import android.os.Binder; 35 import android.os.Bundle; 36 import android.os.UserHandle; 37 import android.os.UserManager; 38 import android.text.TextUtils; 39 import android.text.format.DateUtils; 40 import android.util.Log; 41 import android.view.Menu; 42 import android.view.MenuInflater; 43 import android.view.MenuItem; 44 45 import androidx.appcompat.app.AlertDialog; 46 import androidx.preference.Preference; 47 48 import com.android.settings.R; 49 import com.android.settings.Utils; 50 import com.android.settings.widget.EntityHeaderController; 51 52 import com.google.android.collect.Lists; 53 54 import java.util.ArrayList; 55 import java.util.Date; 56 import java.util.List; 57 58 public class AccountSyncSettings extends AccountPreferenceBase { 59 60 public static final String ACCOUNT_KEY = "account"; 61 private static final int MENU_SYNC_NOW_ID = Menu.FIRST; 62 private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1; 63 private static final int CANT_DO_ONETIME_SYNC_DIALOG = 102; 64 65 private Account mAccount; 66 private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList(); 67 68 @Override onCreateDialog(final int id)69 public Dialog onCreateDialog(final int id) { 70 Dialog dialog = null; 71 if (id == CANT_DO_ONETIME_SYNC_DIALOG) { 72 dialog = new AlertDialog.Builder(getActivity()) 73 .setTitle(R.string.cant_sync_dialog_title) 74 .setMessage(R.string.cant_sync_dialog_message) 75 .setPositiveButton(android.R.string.ok, null) 76 .create(); 77 } 78 return dialog; 79 } 80 81 @Override getMetricsCategory()82 public int getMetricsCategory() { 83 return SettingsEnums.ACCOUNTS_ACCOUNT_SYNC; 84 } 85 86 @Override getDialogMetricsCategory(int dialogId)87 public int getDialogMetricsCategory(int dialogId) { 88 switch (dialogId) { 89 case CANT_DO_ONETIME_SYNC_DIALOG: 90 return SettingsEnums.DIALOG_ACCOUNT_SYNC_CANNOT_ONETIME_SYNC; 91 default: 92 return 0; 93 } 94 } 95 96 @Override onCreate(Bundle icicle)97 public void onCreate(Bundle icicle) { 98 super.onCreate(icicle); 99 addPreferencesFromResource(R.xml.account_sync_settings); 100 getPreferenceScreen().setOrderingAsAdded(false); 101 setAccessibilityTitle(); 102 } 103 104 @Override onActivityCreated(Bundle savedInstanceState)105 public void onActivityCreated(Bundle savedInstanceState) { 106 super.onActivityCreated(savedInstanceState); 107 108 Bundle arguments = getArguments(); 109 if (arguments == null) { 110 Log.e(TAG, "No arguments provided when starting intent. ACCOUNT_KEY needed."); 111 finish(); 112 return; 113 } 114 mAccount = arguments.getParcelable(ACCOUNT_KEY); 115 if (!accountExists(mAccount)) { 116 Log.e(TAG, "Account provided does not exist: " + mAccount); 117 finish(); 118 return; 119 } 120 if (Log.isLoggable(TAG, Log.VERBOSE)) { 121 Log.v(TAG, "Got account: " + mAccount); 122 } 123 final Activity activity = getActivity(); 124 final Preference pref = EntityHeaderController 125 .newInstance(activity, this, null /* header */) 126 .setRecyclerView(getListView(), getSettingsLifecycle()) 127 .setIcon(getDrawableForType(mAccount.type)) 128 .setLabel(mAccount.name) 129 .setSummary(getLabelForType(mAccount.type)) 130 .done(activity, getPrefContext()); 131 pref.setOrder(0); 132 getPreferenceScreen().addPreference(pref); 133 } 134 setAccessibilityTitle()135 private void setAccessibilityTitle() { 136 final UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); 137 UserInfo user = um.getUserInfo(mUserHandle.getIdentifier()); 138 boolean isWorkProfile = user != null ? user.isManagedProfile() : false; 139 CharSequence currentTitle = getActivity().getTitle(); 140 String accessibilityTitle = 141 getString(isWorkProfile 142 ? R.string.accessibility_work_account_title 143 : R.string.accessibility_personal_account_title, currentTitle); 144 getActivity().setTitle(Utils.createAccessibleSequence(currentTitle, accessibilityTitle)); 145 } 146 147 @Override onResume()148 public void onResume() { 149 mAuthenticatorHelper.listenToAccountUpdates(); 150 updateAuthDescriptions(); 151 onAccountsUpdate(Binder.getCallingUserHandle()); 152 super.onResume(); 153 } 154 155 @Override onPause()156 public void onPause() { 157 super.onPause(); 158 mAuthenticatorHelper.stopListeningToAccountUpdates(); 159 } 160 addSyncStateSwitch(Account account, String authority, String packageName, int uid)161 private void addSyncStateSwitch(Account account, String authority, 162 String packageName, int uid) { 163 SyncStateSwitchPreference item = (SyncStateSwitchPreference) getCachedPreference(authority); 164 if (item == null) { 165 item = new SyncStateSwitchPreference(getPrefContext(), account, authority, 166 packageName, uid); 167 getPreferenceScreen().addPreference(item); 168 } else { 169 item.setup(account, authority, packageName, uid); 170 } 171 final PackageManager packageManager = getPackageManager(); 172 item.setPersistent(false); 173 final ProviderInfo providerInfo = packageManager.resolveContentProviderAsUser( 174 authority, 0, mUserHandle.getIdentifier()); 175 if (providerInfo == null) { 176 return; 177 } 178 final CharSequence providerLabel = providerInfo.loadLabel(packageManager); 179 if (TextUtils.isEmpty(providerLabel)) { 180 Log.e(TAG, "Provider needs a label for authority '" + authority + "'"); 181 return; 182 } 183 item.setTitle(providerLabel); 184 item.setKey(authority); 185 } 186 187 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)188 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 189 MenuItem syncNow = menu.add(0, MENU_SYNC_NOW_ID, 0, 190 getString(R.string.sync_menu_sync_now)) 191 .setIcon(R.drawable.ic_menu_refresh_holo_dark); 192 MenuItem syncCancel = menu.add(0, MENU_SYNC_CANCEL_ID, 0, 193 getString(R.string.sync_menu_sync_cancel)) 194 .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel); 195 196 syncNow.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | 197 MenuItem.SHOW_AS_ACTION_WITH_TEXT); 198 syncCancel.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | 199 MenuItem.SHOW_AS_ACTION_WITH_TEXT); 200 201 super.onCreateOptionsMenu(menu, inflater); 202 } 203 204 @Override onPrepareOptionsMenu(Menu menu)205 public void onPrepareOptionsMenu(Menu menu) { 206 super.onPrepareOptionsMenu(menu); 207 // Note that this also counts accounts that are not currently displayed 208 boolean syncActive = !ContentResolver.getCurrentSyncsAsUser( 209 mUserHandle.getIdentifier()).isEmpty(); 210 menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive); 211 menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive); 212 } 213 214 @Override onOptionsItemSelected(MenuItem item)215 public boolean onOptionsItemSelected(MenuItem item) { 216 switch (item.getItemId()) { 217 case MENU_SYNC_NOW_ID: 218 startSyncForEnabledProviders(); 219 return true; 220 case MENU_SYNC_CANCEL_ID: 221 cancelSyncForEnabledProviders(); 222 return true; 223 } 224 return super.onOptionsItemSelected(item); 225 } 226 227 @Override onActivityResult(int requestCode, int resultCode, Intent data)228 public void onActivityResult(int requestCode, int resultCode, Intent data) { 229 if (resultCode == Activity.RESULT_OK) { 230 final int uid = requestCode; 231 final int count = getPreferenceScreen().getPreferenceCount(); 232 for (int i = 0; i < count; i++) { 233 Preference preference = getPreferenceScreen().getPreference(i); 234 if (preference instanceof SyncStateSwitchPreference) { 235 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) preference; 236 if (syncPref.getUid() == uid) { 237 onPreferenceTreeClick(syncPref); 238 return; 239 } 240 } 241 } 242 } 243 } 244 245 @Override onPreferenceTreeClick(Preference preference)246 public boolean onPreferenceTreeClick(Preference preference) { 247 if (getActivity() == null) { 248 return false; 249 } 250 if (preference instanceof SyncStateSwitchPreference) { 251 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) preference; 252 final String authority = syncPref.getAuthority(); 253 if (TextUtils.isEmpty(authority)) { 254 return false; 255 } 256 final Account account = syncPref.getAccount(); 257 final int userId = mUserHandle.getIdentifier(); 258 final String packageName = syncPref.getPackageName(); 259 260 boolean syncAutomatically = ContentResolver.getSyncAutomaticallyAsUser(account, 261 authority, userId); 262 if (syncPref.isOneTimeSyncMode()) { 263 // If the sync adapter doesn't have access to the account we either 264 // request access by starting an activity if possible or kick off the 265 // sync which will end up posting an access request notification. 266 if (requestAccountAccessIfNeeded(packageName)) { 267 return true; 268 } 269 requestOrCancelSync(account, authority, true); 270 } else { 271 boolean syncOn = syncPref.isChecked(); 272 boolean oldSyncState = syncAutomatically; 273 if (syncOn != oldSyncState) { 274 // Toggling this switch triggers sync but we may need a user approval. 275 // If the sync adapter doesn't have access to the account we either 276 // request access by starting an activity if possible or kick off the 277 // sync which will end up posting an access request notification. 278 if (syncOn && requestAccountAccessIfNeeded(packageName)) { 279 return true; 280 } 281 // if we're enabling sync, this will request a sync as well 282 ContentResolver.setSyncAutomaticallyAsUser(account, authority, syncOn, userId); 283 // if the master sync switch is off, the request above will 284 // get dropped. when the user clicks on this toggle, 285 // we want to force the sync, however. 286 if (!ContentResolver.getMasterSyncAutomaticallyAsUser(userId) || !syncOn) { 287 requestOrCancelSync(account, authority, syncOn); 288 } 289 } 290 } 291 return true; 292 } else { 293 return super.onPreferenceTreeClick(preference); 294 } 295 } 296 requestAccountAccessIfNeeded(String packageName)297 private boolean requestAccountAccessIfNeeded(String packageName) { 298 if (packageName == null) { 299 return false; 300 } 301 302 final int uid; 303 try { 304 uid = getContext().getPackageManager().getPackageUidAsUser( 305 packageName, mUserHandle.getIdentifier()); 306 } catch (PackageManager.NameNotFoundException e) { 307 Log.e(TAG, "Invalid sync ", e); 308 return false; 309 } 310 311 AccountManager accountManager = getContext().getSystemService(AccountManager.class); 312 if (!accountManager.hasAccountAccess(mAccount, packageName, mUserHandle)) { 313 IntentSender intent = accountManager.createRequestAccountAccessIntentSenderAsUser( 314 mAccount, packageName, mUserHandle); 315 if (intent != null) { 316 try { 317 startIntentSenderForResult(intent, uid, null, 0, 0, 0, null); 318 return true; 319 } catch (IntentSender.SendIntentException e) { 320 Log.e(TAG, "Error requesting account access", e); 321 } 322 } 323 } 324 return false; 325 } 326 startSyncForEnabledProviders()327 private void startSyncForEnabledProviders() { 328 requestOrCancelSyncForEnabledProviders(true /* start them */); 329 final Activity activity = getActivity(); 330 if (activity != null) { 331 activity.invalidateOptionsMenu(); 332 } 333 } 334 cancelSyncForEnabledProviders()335 private void cancelSyncForEnabledProviders() { 336 requestOrCancelSyncForEnabledProviders(false /* cancel them */); 337 final Activity activity = getActivity(); 338 if (activity != null) { 339 activity.invalidateOptionsMenu(); 340 } 341 } 342 requestOrCancelSyncForEnabledProviders(boolean startSync)343 private void requestOrCancelSyncForEnabledProviders(boolean startSync) { 344 // sync everything that the user has enabled 345 int count = getPreferenceScreen().getPreferenceCount(); 346 for (int i = 0; i < count; i++) { 347 Preference pref = getPreferenceScreen().getPreference(i); 348 if (!(pref instanceof SyncStateSwitchPreference)) { 349 continue; 350 } 351 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) pref; 352 if (!syncPref.isChecked()) { 353 continue; 354 } 355 requestOrCancelSync(syncPref.getAccount(), syncPref.getAuthority(), startSync); 356 } 357 // plus whatever the system needs to sync, e.g., invisible sync adapters 358 if (mAccount != null) { 359 for (SyncAdapterType syncAdapter : mInvisibleAdapters) { 360 requestOrCancelSync(mAccount, syncAdapter.authority, startSync); 361 } 362 } 363 } 364 requestOrCancelSync(Account account, String authority, boolean flag)365 private void requestOrCancelSync(Account account, String authority, boolean flag) { 366 if (flag) { 367 Bundle extras = new Bundle(); 368 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 369 ContentResolver.requestSyncAsUser(account, authority, mUserHandle.getIdentifier(), 370 extras); 371 } else { 372 ContentResolver.cancelSyncAsUser(account, authority, mUserHandle.getIdentifier()); 373 } 374 } 375 isSyncing(List<SyncInfo> currentSyncs, Account account, String authority)376 private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) { 377 for (SyncInfo syncInfo : currentSyncs) { 378 if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) { 379 return true; 380 } 381 } 382 return false; 383 } 384 385 @Override onSyncStateUpdated()386 protected void onSyncStateUpdated() { 387 if (!isResumed()) return; 388 setFeedsState(); 389 final Activity activity = getActivity(); 390 if (activity != null) { 391 activity.invalidateOptionsMenu(); 392 } 393 } 394 setFeedsState()395 private void setFeedsState() { 396 // iterate over all the preferences, setting the state properly for each 397 Date date = new Date(); 398 final int userId = mUserHandle.getIdentifier(); 399 List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId); 400 boolean syncIsFailing = false; 401 402 // Refresh the sync status switches - some syncs may have become active. 403 updateAccountSwitches(); 404 405 for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) { 406 Preference pref = getPreferenceScreen().getPreference(i); 407 if (!(pref instanceof SyncStateSwitchPreference)) { 408 continue; 409 } 410 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) pref; 411 412 String authority = syncPref.getAuthority(); 413 Account account = syncPref.getAccount(); 414 415 SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority, userId); 416 boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority, 417 userId); 418 boolean authorityIsPending = status == null ? false : status.pending; 419 boolean initialSync = status == null ? false : status.initialize; 420 421 boolean activelySyncing = isSyncing(currentSyncs, account, authority); 422 boolean lastSyncFailed = status != null 423 && status.lastFailureTime != 0 424 && status.getLastFailureMesgAsInt(0) 425 != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; 426 if (!syncEnabled) lastSyncFailed = false; 427 if (lastSyncFailed && !activelySyncing && !authorityIsPending) { 428 syncIsFailing = true; 429 } 430 if (Log.isLoggable(TAG, Log.DEBUG)) { 431 Log.d(TAG, "Update sync status: " + account + " " + authority + 432 " active = " + activelySyncing + " pend =" + authorityIsPending); 433 } 434 435 final long successEndTime = (status == null) ? 0 : status.lastSuccessTime; 436 if (!syncEnabled) { 437 syncPref.setSummary(R.string.sync_disabled); 438 } else if (activelySyncing) { 439 syncPref.setSummary(R.string.sync_in_progress); 440 } else if (successEndTime != 0) { 441 date.setTime(successEndTime); 442 final String timeString = formatSyncDate(getContext(), date); 443 syncPref.setSummary(getResources().getString(R.string.last_synced, timeString)); 444 } else { 445 syncPref.setSummary(""); 446 } 447 int syncState = ContentResolver.getIsSyncableAsUser(account, authority, userId); 448 449 syncPref.setActive(activelySyncing && (syncState >= 0) && 450 !initialSync); 451 syncPref.setPending(authorityIsPending && (syncState >= 0) && 452 !initialSync); 453 454 syncPref.setFailed(lastSyncFailed); 455 final boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser( 456 userId); 457 syncPref.setOneTimeSyncMode(oneTimeSyncMode); 458 syncPref.setChecked(oneTimeSyncMode || syncEnabled); 459 } 460 if (syncIsFailing) { 461 mFooterPreferenceMixin.createFooterPreference() 462 .setTitle(R.string.sync_is_failing); 463 } 464 } 465 466 @Override onAccountsUpdate(final UserHandle userHandle)467 public void onAccountsUpdate(final UserHandle userHandle) { 468 super.onAccountsUpdate(userHandle); 469 if (!accountExists(mAccount)) { 470 // The account was deleted 471 finish(); 472 return; 473 } 474 updateAccountSwitches(); 475 onSyncStateUpdated(); 476 } 477 accountExists(Account account)478 private boolean accountExists(Account account) { 479 if (account == null) return false; 480 481 Account[] accounts = AccountManager.get(getActivity()).getAccountsByTypeAsUser( 482 account.type, mUserHandle); 483 final int count = accounts.length; 484 for (int i = 0; i < count; i++) { 485 if (accounts[i].equals(account)) { 486 return true; 487 } 488 } 489 return false; 490 } 491 updateAccountSwitches()492 private void updateAccountSwitches() { 493 mInvisibleAdapters.clear(); 494 495 SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( 496 mUserHandle.getIdentifier()); 497 ArrayList<SyncAdapterType> authorities = new ArrayList<>(); 498 for (int i = 0, n = syncAdapters.length; i < n; i++) { 499 final SyncAdapterType sa = syncAdapters[i]; 500 // Only keep track of sync adapters for this account 501 if (!sa.accountType.equals(mAccount.type)) continue; 502 if (sa.isUserVisible()) { 503 if (Log.isLoggable(TAG, Log.DEBUG)) { 504 Log.d(TAG, "updateAccountSwitches: added authority " + sa.authority 505 + " to accountType " + sa.accountType); 506 } 507 authorities.add(sa); 508 } else { 509 // keep track of invisible sync adapters, so sync now forces 510 // them to sync as well. 511 mInvisibleAdapters.add(sa); 512 } 513 } 514 515 if (Log.isLoggable(TAG, Log.DEBUG)) { 516 Log.d(TAG, "looking for sync adapters that match account " + mAccount); 517 } 518 519 cacheRemoveAllPrefs(getPreferenceScreen()); 520 getCachedPreference(EntityHeaderController.PREF_KEY_APP_HEADER); 521 for (int j = 0, m = authorities.size(); j < m; j++) { 522 final SyncAdapterType syncAdapter = authorities.get(j); 523 // We could check services here.... 524 int syncState = ContentResolver.getIsSyncableAsUser(mAccount, syncAdapter.authority, 525 mUserHandle.getIdentifier()); 526 if (Log.isLoggable(TAG, Log.DEBUG)) { 527 Log.d(TAG, " found authority " + syncAdapter.authority + " " + syncState); 528 } 529 if (syncState > 0) { 530 final int uid; 531 try { 532 uid = getContext().getPackageManager().getPackageUidAsUser( 533 syncAdapter.getPackageName(), mUserHandle.getIdentifier()); 534 addSyncStateSwitch(mAccount, syncAdapter.authority, 535 syncAdapter.getPackageName(), uid); 536 } catch (PackageManager.NameNotFoundException e) { 537 Log.e(TAG, "No uid for package" + syncAdapter.getPackageName(), e); 538 } 539 } 540 } 541 removeCachedPrefs(getPreferenceScreen()); 542 } 543 544 @Override getHelpResource()545 public int getHelpResource() { 546 return R.string.help_url_accounts; 547 } 548 formatSyncDate(Context context, Date date)549 private static String formatSyncDate(Context context, Date date) { 550 return DateUtils.formatDateTime(context, date.getTime(), 551 DateUtils.FORMAT_SHOW_DATE 552 | DateUtils.FORMAT_SHOW_YEAR 553 | DateUtils.FORMAT_SHOW_TIME); 554 } 555 } 556