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 17package com.android.settings.accounts; 18 19import android.accounts.Account; 20import android.accounts.AccountManager; 21import android.accounts.AccountManagerCallback; 22import android.accounts.AccountManagerFuture; 23import android.accounts.AuthenticatorException; 24import android.accounts.OperationCanceledException; 25import android.app.Activity; 26import android.app.AlertDialog; 27import android.app.Dialog; 28import android.content.ContentResolver; 29import android.content.Context; 30import android.content.DialogInterface; 31import android.content.Intent; 32import android.content.SyncAdapterType; 33import android.content.SyncInfo; 34import android.content.SyncStatusInfo; 35import android.content.pm.ProviderInfo; 36import android.net.ConnectivityManager; 37import android.os.Bundle; 38import android.preference.Preference; 39import android.preference.PreferenceScreen; 40import android.text.TextUtils; 41import android.text.format.DateFormat; 42import android.util.Log; 43import android.view.LayoutInflater; 44import android.view.Menu; 45import android.view.MenuInflater; 46import android.view.MenuItem; 47import android.view.View; 48import android.view.ViewGroup; 49import android.widget.ImageView; 50import android.widget.ListView; 51import android.widget.TextView; 52 53import com.android.settings.R; 54import com.android.settings.Utils; 55import com.google.android.collect.Lists; 56import com.google.android.collect.Maps; 57 58import java.io.IOException; 59import java.util.ArrayList; 60import java.util.Collections; 61import java.util.Date; 62import java.util.HashMap; 63import java.util.List; 64 65public class AccountSyncSettings extends AccountPreferenceBase { 66 67 public static final String ACCOUNT_KEY = "account"; 68 private static final int MENU_SYNC_NOW_ID = Menu.FIRST; 69 private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1; 70 private static final int MENU_REMOVE_ACCOUNT_ID = Menu.FIRST + 2; 71 private static final int REALLY_REMOVE_DIALOG = 100; 72 private static final int FAILED_REMOVAL_DIALOG = 101; 73 private static final int CANT_DO_ONETIME_SYNC_DIALOG = 102; 74 private TextView mUserId; 75 private TextView mProviderId; 76 private ImageView mProviderIcon; 77 private TextView mErrorInfoView; 78 private Account mAccount; 79 // List of all accounts, updated when accounts are added/removed 80 // We need to re-scan the accounts on sync events, in case sync state changes. 81 private Account[] mAccounts; 82 private ArrayList<SyncStateCheckBoxPreference> mCheckBoxes = 83 new ArrayList<SyncStateCheckBoxPreference>(); 84 private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList(); 85 86 @Override 87 public Dialog onCreateDialog(final int id) { 88 Dialog dialog = null; 89 if (id == REALLY_REMOVE_DIALOG) { 90 dialog = new AlertDialog.Builder(getActivity()) 91 .setTitle(R.string.really_remove_account_title) 92 .setMessage(R.string.really_remove_account_message) 93 .setNegativeButton(android.R.string.cancel, null) 94 .setPositiveButton(R.string.remove_account_label, 95 new DialogInterface.OnClickListener() { 96 public void onClick(DialogInterface dialog, int which) { 97 AccountManager.get(AccountSyncSettings.this.getActivity()) 98 .removeAccount(mAccount, 99 new AccountManagerCallback<Boolean>() { 100 public void run(AccountManagerFuture<Boolean> future) { 101 // If already out of this screen, don't proceed. 102 if (!AccountSyncSettings.this.isResumed()) { 103 return; 104 } 105 boolean failed = true; 106 try { 107 if (future.getResult() == true) { 108 failed = false; 109 } 110 } catch (OperationCanceledException e) { 111 // handled below 112 } catch (IOException e) { 113 // handled below 114 } catch (AuthenticatorException e) { 115 // handled below 116 } 117 if (failed) { 118 showDialog(FAILED_REMOVAL_DIALOG); 119 } else { 120 finish(); 121 } 122 } 123 }, null); 124 } 125 }) 126 .create(); 127 } else if (id == FAILED_REMOVAL_DIALOG) { 128 dialog = new AlertDialog.Builder(getActivity()) 129 .setTitle(R.string.really_remove_account_title) 130 .setPositiveButton(android.R.string.ok, null) 131 .setMessage(R.string.remove_account_failed) 132 .create(); 133 } else if (id == CANT_DO_ONETIME_SYNC_DIALOG) { 134 dialog = new AlertDialog.Builder(getActivity()) 135 .setTitle(R.string.cant_sync_dialog_title) 136 .setMessage(R.string.cant_sync_dialog_message) 137 .setPositiveButton(android.R.string.ok, null) 138 .create(); 139 } 140 return dialog; 141 } 142 143 @Override 144 public void onCreate(Bundle icicle) { 145 super.onCreate(icicle); 146 147 setHasOptionsMenu(true); 148 } 149 150 @Override 151 public View onCreateView(LayoutInflater inflater, ViewGroup container, 152 Bundle savedInstanceState) { 153 final View view = inflater.inflate(R.layout.account_sync_screen, container, false); 154 155 final ListView list = (ListView) view.findViewById(android.R.id.list); 156 Utils.prepareCustomPreferencesList(container, view, list, false); 157 158 initializeUi(view); 159 160 return view; 161 } 162 163 protected void initializeUi(final View rootView) { 164 addPreferencesFromResource(R.xml.account_sync_settings); 165 166 mErrorInfoView = (TextView) rootView.findViewById(R.id.sync_settings_error_info); 167 mErrorInfoView.setVisibility(View.GONE); 168 169 mUserId = (TextView) rootView.findViewById(R.id.user_id); 170 mProviderId = (TextView) rootView.findViewById(R.id.provider_id); 171 mProviderIcon = (ImageView) rootView.findViewById(R.id.provider_icon); 172 } 173 174 @Override 175 public void onActivityCreated(Bundle savedInstanceState) { 176 super.onActivityCreated(savedInstanceState); 177 178 Bundle arguments = getArguments(); 179 if (arguments == null) { 180 Log.e(TAG, "No arguments provided when starting intent. ACCOUNT_KEY needed."); 181 return; 182 } 183 184 mAccount = (Account) arguments.getParcelable(ACCOUNT_KEY); 185 if (mAccount != null) { 186 if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Got account: " + mAccount); 187 mUserId.setText(mAccount.name); 188 mProviderId.setText(mAccount.type); 189 } 190 } 191 192 @Override 193 public void onResume() { 194 final Activity activity = getActivity(); 195 AccountManager.get(activity).addOnAccountsUpdatedListener(this, null, false); 196 updateAuthDescriptions(); 197 onAccountsUpdated(AccountManager.get(activity).getAccounts()); 198 199 super.onResume(); 200 } 201 202 @Override 203 public void onPause() { 204 super.onPause(); 205 AccountManager.get(getActivity()).removeOnAccountsUpdatedListener(this); 206 } 207 208 private void addSyncStateCheckBox(Account account, String authority) { 209 SyncStateCheckBoxPreference item = 210 new SyncStateCheckBoxPreference(getActivity(), account, authority); 211 item.setPersistent(false); 212 final ProviderInfo providerInfo = getPackageManager().resolveContentProvider(authority, 0); 213 if (providerInfo == null) { 214 return; 215 } 216 CharSequence providerLabel = providerInfo.loadLabel(getPackageManager()); 217 if (TextUtils.isEmpty(providerLabel)) { 218 Log.e(TAG, "Provider needs a label for authority '" + authority + "'"); 219 return; 220 } 221 String title = getString(R.string.sync_item_title, providerLabel); 222 item.setTitle(title); 223 item.setKey(authority); 224 mCheckBoxes.add(item); 225 } 226 227 @Override 228 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 229 230 MenuItem syncNow = menu.add(0, MENU_SYNC_NOW_ID, 0, 231 getString(R.string.sync_menu_sync_now)) 232 .setIcon(R.drawable.ic_menu_refresh_holo_dark); 233 MenuItem syncCancel = menu.add(0, MENU_SYNC_CANCEL_ID, 0, 234 getString(R.string.sync_menu_sync_cancel)) 235 .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel); 236 MenuItem removeAccount = menu.add(0, MENU_REMOVE_ACCOUNT_ID, 0, 237 getString(R.string.remove_account_label)) 238 .setIcon(R.drawable.ic_menu_delete_holo_dark); 239 240 removeAccount.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | 241 MenuItem.SHOW_AS_ACTION_WITH_TEXT); 242 syncNow.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | 243 MenuItem.SHOW_AS_ACTION_WITH_TEXT); 244 syncCancel.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | 245 MenuItem.SHOW_AS_ACTION_WITH_TEXT); 246 247 super.onCreateOptionsMenu(menu, inflater); 248 } 249 250 @Override 251 public void onPrepareOptionsMenu(Menu menu) { 252 super.onPrepareOptionsMenu(menu); 253 boolean syncActive = ContentResolver.getCurrentSync() != null; 254 menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive); 255 menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive); 256 } 257 258 @Override 259 public boolean onOptionsItemSelected(MenuItem item) { 260 switch (item.getItemId()) { 261 case MENU_SYNC_NOW_ID: 262 startSyncForEnabledProviders(); 263 return true; 264 case MENU_SYNC_CANCEL_ID: 265 cancelSyncForEnabledProviders(); 266 return true; 267 case MENU_REMOVE_ACCOUNT_ID: 268 showDialog(REALLY_REMOVE_DIALOG); 269 return true; 270 } 271 return super.onOptionsItemSelected(item); 272 } 273 274 @Override 275 public boolean onPreferenceTreeClick(PreferenceScreen preferences, Preference preference) { 276 if (preference instanceof SyncStateCheckBoxPreference) { 277 SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) preference; 278 String authority = syncPref.getAuthority(); 279 Account account = syncPref.getAccount(); 280 boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority); 281 if (syncPref.isOneTimeSyncMode()) { 282 requestOrCancelSync(account, authority, true); 283 } else { 284 boolean syncOn = syncPref.isChecked(); 285 boolean oldSyncState = syncAutomatically; 286 if (syncOn != oldSyncState) { 287 // if we're enabling sync, this will request a sync as well 288 ContentResolver.setSyncAutomatically(account, authority, syncOn); 289 // if the master sync switch is off, the request above will 290 // get dropped. when the user clicks on this toggle, 291 // we want to force the sync, however. 292 if (!ContentResolver.getMasterSyncAutomatically() || !syncOn) { 293 requestOrCancelSync(account, authority, syncOn); 294 } 295 } 296 } 297 return true; 298 } else { 299 return super.onPreferenceTreeClick(preferences, preference); 300 } 301 } 302 303 private void startSyncForEnabledProviders() { 304 requestOrCancelSyncForEnabledProviders(true /* start them */); 305 getActivity().invalidateOptionsMenu(); 306 } 307 308 private void cancelSyncForEnabledProviders() { 309 requestOrCancelSyncForEnabledProviders(false /* cancel them */); 310 getActivity().invalidateOptionsMenu(); 311 } 312 313 private void requestOrCancelSyncForEnabledProviders(boolean startSync) { 314 // sync everything that the user has enabled 315 int count = getPreferenceScreen().getPreferenceCount(); 316 for (int i = 0; i < count; i++) { 317 Preference pref = getPreferenceScreen().getPreference(i); 318 if (! (pref instanceof SyncStateCheckBoxPreference)) { 319 continue; 320 } 321 SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) pref; 322 if (!syncPref.isChecked()) { 323 continue; 324 } 325 requestOrCancelSync(syncPref.getAccount(), syncPref.getAuthority(), startSync); 326 } 327 // plus whatever the system needs to sync, e.g., invisible sync adapters 328 if (mAccount != null) { 329 for (SyncAdapterType syncAdapter : mInvisibleAdapters) { 330 // invisible sync adapters' account type should be same as current account type 331 if (syncAdapter.accountType.equals(mAccount.type)) { 332 requestOrCancelSync(mAccount, syncAdapter.authority, startSync); 333 } 334 } 335 } 336 } 337 338 private void requestOrCancelSync(Account account, String authority, boolean flag) { 339 if (flag) { 340 Bundle extras = new Bundle(); 341 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 342 ContentResolver.requestSync(account, authority, extras); 343 } else { 344 ContentResolver.cancelSync(account, authority); 345 } 346 } 347 348 private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) { 349 for (SyncInfo syncInfo : currentSyncs) { 350 if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) { 351 return true; 352 } 353 } 354 return false; 355 } 356 357 @Override 358 protected void onSyncStateUpdated() { 359 if (!isResumed()) return; 360 setFeedsState(); 361 } 362 363 private void setFeedsState() { 364 // iterate over all the preferences, setting the state properly for each 365 Date date = new Date(); 366 List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncs(); 367 boolean syncIsFailing = false; 368 369 // Refresh the sync status checkboxes - some syncs may have become active. 370 updateAccountCheckboxes(mAccounts); 371 372 for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) { 373 Preference pref = getPreferenceScreen().getPreference(i); 374 if (! (pref instanceof SyncStateCheckBoxPreference)) { 375 continue; 376 } 377 SyncStateCheckBoxPreference syncPref = (SyncStateCheckBoxPreference) pref; 378 379 String authority = syncPref.getAuthority(); 380 Account account = syncPref.getAccount(); 381 382 SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority); 383 boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); 384 boolean authorityIsPending = status == null ? false : status.pending; 385 boolean initialSync = status == null ? false : status.initialize; 386 387 boolean activelySyncing = isSyncing(currentSyncs, account, authority); 388 boolean lastSyncFailed = status != null 389 && status.lastFailureTime != 0 390 && status.getLastFailureMesgAsInt(0) 391 != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; 392 if (!syncEnabled) lastSyncFailed = false; 393 if (lastSyncFailed && !activelySyncing && !authorityIsPending) { 394 syncIsFailing = true; 395 } 396 if (Log.isLoggable(TAG, Log.VERBOSE)) { 397 Log.d(TAG, "Update sync status: " + account + " " + authority + 398 " active = " + activelySyncing + " pend =" + authorityIsPending); 399 } 400 401 final long successEndTime = (status == null) ? 0 : status.lastSuccessTime; 402 if (!syncEnabled) { 403 syncPref.setSummary(R.string.sync_disabled); 404 } else if (activelySyncing) { 405 syncPref.setSummary(R.string.sync_in_progress); 406 } else if (successEndTime != 0) { 407 date.setTime(successEndTime); 408 final String timeString = formatSyncDate(date); 409 syncPref.setSummary(getResources().getString(R.string.last_synced, timeString)); 410 } else { 411 syncPref.setSummary(""); 412 } 413 int syncState = ContentResolver.getIsSyncable(account, authority); 414 415 syncPref.setActive(activelySyncing && (syncState >= 0) && 416 !initialSync); 417 syncPref.setPending(authorityIsPending && (syncState >= 0) && 418 !initialSync); 419 420 syncPref.setFailed(lastSyncFailed); 421 ConnectivityManager connManager = 422 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 423 final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); 424 final boolean backgroundDataEnabled = connManager.getBackgroundDataSetting(); 425 final boolean oneTimeSyncMode = !masterSyncAutomatically || !backgroundDataEnabled; 426 syncPref.setOneTimeSyncMode(oneTimeSyncMode); 427 syncPref.setChecked(oneTimeSyncMode || syncEnabled); 428 } 429 mErrorInfoView.setVisibility(syncIsFailing ? View.VISIBLE : View.GONE); 430 getActivity().invalidateOptionsMenu(); 431 } 432 433 @Override 434 public void onAccountsUpdated(Account[] accounts) { 435 super.onAccountsUpdated(accounts); 436 mAccounts = accounts; 437 updateAccountCheckboxes(accounts); 438 onSyncStateUpdated(); 439 } 440 441 private void updateAccountCheckboxes(Account[] accounts) { 442 mInvisibleAdapters.clear(); 443 444 SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); 445 HashMap<String, ArrayList<String>> accountTypeToAuthorities = 446 Maps.newHashMap(); 447 for (int i = 0, n = syncAdapters.length; i < n; i++) { 448 final SyncAdapterType sa = syncAdapters[i]; 449 if (sa.isUserVisible()) { 450 ArrayList<String> authorities = accountTypeToAuthorities.get(sa.accountType); 451 if (authorities == null) { 452 authorities = new ArrayList<String>(); 453 accountTypeToAuthorities.put(sa.accountType, authorities); 454 } 455 if (Log.isLoggable(TAG, Log.VERBOSE)) { 456 Log.d(TAG, "onAccountUpdated: added authority " + sa.authority 457 + " to accountType " + sa.accountType); 458 } 459 authorities.add(sa.authority); 460 } else { 461 // keep track of invisible sync adapters, so sync now forces 462 // them to sync as well. 463 mInvisibleAdapters.add(sa); 464 } 465 } 466 467 for (int i = 0, n = mCheckBoxes.size(); i < n; i++) { 468 getPreferenceScreen().removePreference(mCheckBoxes.get(i)); 469 } 470 mCheckBoxes.clear(); 471 472 for (int i = 0, n = accounts.length; i < n; i++) { 473 final Account account = accounts[i]; 474 if (Log.isLoggable(TAG, Log.VERBOSE)) { 475 Log.d(TAG, "looking for sync adapters that match account " + account); 476 } 477 final ArrayList<String> authorities = accountTypeToAuthorities.get(account.type); 478 if (authorities != null && (mAccount == null || mAccount.equals(account))) { 479 for (int j = 0, m = authorities.size(); j < m; j++) { 480 final String authority = authorities.get(j); 481 // We could check services here.... 482 int syncState = ContentResolver.getIsSyncable(account, authority); 483 if (Log.isLoggable(TAG, Log.VERBOSE)) { 484 Log.d(TAG, " found authority " + authority + " " + syncState); 485 } 486 if (syncState > 0) { 487 addSyncStateCheckBox(account, authority); 488 } 489 } 490 } 491 } 492 493 Collections.sort(mCheckBoxes); 494 for (int i = 0, n = mCheckBoxes.size(); i < n; i++) { 495 getPreferenceScreen().addPreference(mCheckBoxes.get(i)); 496 } 497 } 498 499 /** 500 * Updates the titlebar with an icon for the provider type. 501 */ 502 @Override 503 protected void onAuthDescriptionsUpdated() { 504 super.onAuthDescriptionsUpdated(); 505 getPreferenceScreen().removeAll(); 506 if (mAccount != null) { 507 mProviderIcon.setImageDrawable(getDrawableForType(mAccount.type)); 508 mProviderId.setText(getLabelForType(mAccount.type)); 509 } 510 addPreferencesFromResource(R.xml.account_sync_settings); 511 } 512 513 @Override 514 protected int getHelpResource() { 515 return R.string.help_url_accounts; 516 } 517} 518