1/* 2 * Copyright (C) 2014 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.tv.settings.accounts; 18 19import com.android.tv.settings.R; 20import com.android.tv.settings.dialog.old.Action; 21import com.android.tv.settings.dialog.old.ActionAdapter; 22import com.android.tv.settings.dialog.old.ActionFragment; 23import com.android.tv.settings.dialog.old.ContentFragment; 24import com.android.tv.settings.dialog.old.DialogActivity; 25 26import android.accounts.Account; 27import android.accounts.AccountManager; 28import android.accounts.OnAccountsUpdateListener; 29import android.content.ContentResolver; 30import android.content.Context; 31import android.content.Intent; 32import android.content.SyncAdapterType; 33import android.content.SyncInfo; 34import android.content.SyncStatusInfo; 35import android.content.SyncStatusObserver; 36import android.content.pm.ProviderInfo; 37import android.net.ConnectivityManager; 38import android.os.AsyncTask; 39import android.os.Bundle; 40import android.preference.Preference; 41import android.text.TextUtils; 42import android.text.format.DateUtils; 43import android.util.Log; 44 45import java.util.ArrayList; 46import java.util.Collections; 47import java.util.Comparator; 48import java.util.Date; 49import java.util.HashMap; 50import java.util.List; 51 52/** 53 * Displays the sync settings for a given account. 54 */ 55public class AccountSyncSettings extends DialogActivity implements OnAccountsUpdateListener { 56 57 private static final String TAG = "AccountSyncSettings"; 58 59 private static final String KEY_SYNC_NOW = "KEY_SYNC_NOW"; 60 private static final String KEY_CANCEL_SYNC = "KEY_SYNC_CANCEL"; 61 62 private static final String EXTRA_ONE_TIME_SYNC = "one_time_sync"; 63 private static final String EXTRA_ACCOUNT = "account"; 64 65 private AccountManager mAccountManager; 66 private Account mAccount; 67 private AuthenticatorHelper mHelper; 68 69 /** 70 * Adapters which are invisible. Store them so that sync now syncs everything. 71 */ 72 private List<SyncAdapterType> mInvisibleAdapters; 73 74 private Account[] mAccounts; 75 76 private boolean mSyncIsFailing; 77 78 private Object mStatusChangeListenerHandle; 79 80 private ActionFragment mActionFragment; 81 82 private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() { 83 84 @Override 85 public void onStatusChanged(int which) { 86 onSyncStateUpdated(); 87 } 88 }; 89 90 @Override 91 protected void onCreate(Bundle savedInstanceState) { 92 super.onCreate(savedInstanceState); 93 mHelper = new AuthenticatorHelper(); 94 mAccountManager = AccountManager.get(this); 95 mInvisibleAdapters = new ArrayList<SyncAdapterType>(); 96 String accountName = getIntent().getStringExtra(AccountSettingsActivity.EXTRA_ACCOUNT); 97 if (!TextUtils.isEmpty(accountName)) { 98 // Search for the account. 99 for (Account account : mAccountManager.getAccounts()) { 100 if (account.name.equals(accountName)) { 101 mAccount = account; 102 break; 103 } 104 } 105 } 106 if (mAccount == null) { 107 finish(); 108 return; 109 } 110 mActionFragment = ActionFragment.newInstance(getActions(mAccountManager.getAccounts())); 111 // Start with an empty list and then fill in with the sync adapters. 112 setContentAndActionFragments(ContentFragment.newInstance( 113 accountName, mAccount.type, "", R.drawable.ic_settings_sync, 114 getResources().getColor(R.color.icon_background)), mActionFragment); 115 } 116 117 @Override 118 protected void onResume() { 119 super.onResume(); 120 AccountManager.get(this).addOnAccountsUpdatedListener(this, null, false); 121 updateAuthDescriptions(); 122 onAccountsUpdated(AccountManager.get(this).getAccounts()); 123 mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener( 124 ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE 125 | ContentResolver.SYNC_OBSERVER_TYPE_STATUS 126 | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver); 127 } 128 129 @Override 130 public void onPause() { 131 ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle); 132 AccountManager.get(this).removeOnAccountsUpdatedListener(this); 133 super.onPause(); 134 } 135 136 @Override 137 public void onAccountsUpdated(Account[] accounts) { 138 mAccounts = accounts; 139 loadSyncActions(accounts); 140 } 141 142 @Override 143 public void onActionClicked(Action action) { 144 String key = action.getKey(); 145 if (KEY_SYNC_NOW.equals(key)) { 146 startSyncForEnabledProviders(); 147 } else if (KEY_CANCEL_SYNC.equals(key)) { 148 cancelSyncForEnabledProviders(); 149 } else { 150 // This is a specific sync adapter. 151 Account account = action.getIntent().getParcelableExtra(EXTRA_ACCOUNT); 152 String authority = action.getKey(); 153 boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority); 154 if (action.getIntent().getBooleanExtra(EXTRA_ONE_TIME_SYNC, false)) { 155 requestOrCancelSync(account, authority, true); 156 } else { 157 boolean syncOn = !action.isChecked(); // toggle 158 boolean oldSyncState = syncAutomatically; 159 if (syncOn != oldSyncState) { 160 // if we're enabling sync, this will request a sync as well 161 ContentResolver.setSyncAutomatically(account, authority, syncOn); 162 // if the master sync switch is off, the request above will 163 // get dropped. when the user clicks on this toggle, 164 // we want to force the sync, however. 165 if (!ContentResolver.getMasterSyncAutomatically() || !syncOn) { 166 requestOrCancelSync(account, authority, syncOn); 167 } 168 } 169 if (mAccounts != null) { 170 loadSyncActions(mAccounts); 171 } 172 } 173 } 174 } 175 176 private void onSyncStateUpdated() { 177 if (!isResumed() || mAccounts == null) { 178 return; 179 } 180 loadSyncActions(mAccounts); 181 } 182 183 private void updateAuthDescriptions() { 184 mHelper.updateAuthDescriptions(this); 185 onAuthDescriptionsUpdated(); 186 } 187 188 private void onAuthDescriptionsUpdated() { 189 ((ContentFragment) getContentFragment()).setBreadCrumbText( 190 mHelper.getLabelForType(this, mAccount.type).toString()); 191 } 192 193 194 private void loadSyncActions(Account[] accounts) { 195 new LoadActionsTask().execute(accounts); 196 } 197 198 private ArrayList<Action> getActions(Account[] accounts) { 199 ArrayList<Action> actions = new ArrayList<Action>(); 200 201 Date date = new Date(); 202 List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncs(); 203 mSyncIsFailing = false; 204 205 mInvisibleAdapters.clear(); 206 207 SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypes(); 208 HashMap<String, ArrayList<String>> accountTypeToAuthorities = 209 new HashMap<String, ArrayList<String>>(); 210 for (int i = 0, n = syncAdapters.length; i < n; i++) { 211 final SyncAdapterType sa = syncAdapters[i]; 212 if (sa.isUserVisible()) { 213 ArrayList<String> authorities = accountTypeToAuthorities.get(sa.accountType); 214 if (authorities == null) { 215 authorities = new ArrayList<String>(); 216 accountTypeToAuthorities.put(sa.accountType, authorities); 217 } 218 if (Log.isLoggable(TAG, Log.VERBOSE)) { 219 Log.d(TAG, "onAccountUpdated: added authority " + sa.authority 220 + " to accountType " + sa.accountType); 221 } 222 authorities.add(sa.authority); 223 } else { 224 // keep track of invisible sync adapters, so sync now forces 225 // them to sync as well. 226 mInvisibleAdapters.add(sa); 227 } 228 } 229 230 for (int i = 0, n = accounts.length; i < n; i++) { 231 final Account account = accounts[i]; 232 if (Log.isLoggable(TAG, Log.VERBOSE)) { 233 Log.d(TAG, "looking for sync adapters that match account " + account); 234 } 235 final ArrayList<String> authorities = accountTypeToAuthorities.get(account.type); 236 if (authorities != null && (mAccount == null || mAccount.equals(account))) { 237 for (int j = 0, m = authorities.size(); j < m; j++) { 238 final String authority = authorities.get(j); 239 // We could check services here.... 240 int syncState = ContentResolver.getIsSyncable(account, authority); 241 if (Log.isLoggable(TAG, Log.VERBOSE)) { 242 Log.d(TAG, " found authority " + authority + " " + syncState); 243 } 244 if (syncState > 0) { 245 Action action = getAction(account, authority, currentSyncs); 246 if (action != null) { 247 actions.add(action); 248 } 249 } 250 } 251 } 252 } 253 254 Collections.sort(actions, ADAPTER_COMPARATOR); 255 // Always add a "Sync now | cancel sync" action at the beginning. 256 boolean syncActive = false; 257 List<SyncInfo> syncList = ContentResolver.getCurrentSyncs(); 258 for (SyncInfo info : syncList) { 259 if (mAccount.equals(info.account)) { 260 syncActive = true; 261 break; 262 } 263 } 264 265 actions.add(0, new Action.Builder() 266 .key(!syncActive ? KEY_SYNC_NOW : KEY_CANCEL_SYNC) 267 .title(getString(!syncActive ? R.string.sync_now : R.string.sync_cancel)) 268 .build()); 269 return actions; 270 } 271 272 /** 273 * Gets an action item with the appropriate description / checkmark / drawable. 274 * <p> 275 * Returns null if the provider can't be shown for some reason. 276 */ 277 private Action getAction(Account account, String authority, List<SyncInfo> currentSyncs) { 278 final ProviderInfo providerInfo = getPackageManager().resolveContentProvider(authority, 0); 279 if (providerInfo == null) { 280 return null; 281 } 282 CharSequence providerLabel = providerInfo.loadLabel(getPackageManager()); 283 if (TextUtils.isEmpty(providerLabel)) { 284 return null; 285 } 286 String description; 287 boolean isSyncing; 288 Date date = new Date(); 289 SyncStatusInfo status = ContentResolver.getSyncStatus(account, authority); 290 boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority); 291 boolean authorityIsPending = status == null ? false : status.pending; 292 boolean initialSync = status == null ? false : status.initialize; 293 294 boolean activelySyncing = isSyncing(currentSyncs, account, authority); 295 boolean lastSyncFailed = status != null 296 && status.lastFailureTime != 0 297 && status.getLastFailureMesgAsInt(0) 298 != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; 299 if (!syncEnabled) lastSyncFailed = false; 300 if (lastSyncFailed && !activelySyncing && !authorityIsPending) { 301 mSyncIsFailing = true; 302 } 303 if (Log.isLoggable(TAG, Log.VERBOSE)) { 304 Log.d(TAG, "Update sync status: " + account + " " + authority + 305 " active = " + activelySyncing + " pend =" + authorityIsPending); 306 } 307 308 final long successEndTime = (status == null) ? 0 : status.lastSuccessTime; 309 if (!syncEnabled) { 310 description = getString(R.string.sync_disabled); 311 } else if (activelySyncing) { 312 description = getString(R.string.sync_in_progress); 313 } else if (successEndTime != 0) { 314 date.setTime(successEndTime); 315 final String timeString = formatSyncDate(date); 316 description = getString(R.string.last_synced, timeString); 317 } else { 318 description = ""; 319 } 320 int syncState = ContentResolver.getIsSyncable(account, authority); 321 322 boolean pending = authorityIsPending && (syncState >= 0) && !initialSync; 323 boolean active = activelySyncing && (syncState >= 0) && !initialSync; 324 boolean activeVisible = pending || active; 325 // TODO: set drawable based on these flags. 326 327 ConnectivityManager connManager = 328 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 329 final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically(); 330 final boolean backgroundDataEnabled = connManager.getBackgroundDataSetting(); 331 final boolean oneTimeSyncMode = !masterSyncAutomatically || !backgroundDataEnabled; 332 boolean checked = oneTimeSyncMode || syncEnabled; 333 334 // Store extras in the intent 335 Intent intent = new Intent() 336 .putExtra(EXTRA_ONE_TIME_SYNC, oneTimeSyncMode) 337 .putExtra(EXTRA_ACCOUNT, account); 338 339 if (Log.isLoggable(TAG, Log.VERBOSE)) { 340 Log.d(TAG, "Creating action " + providerLabel.toString() + " " + description); 341 } 342 return new Action.Builder() 343 .key(authority) 344 .title(providerLabel.toString()) 345 .description(description) 346 .checked(checked) 347 .intent(intent) 348 .build(); 349 } 350 351 private void startSyncForEnabledProviders() { 352 requestOrCancelSyncForEnabledProviders(true /* start them */); 353 } 354 355 private void cancelSyncForEnabledProviders() { 356 requestOrCancelSyncForEnabledProviders(false /* cancel them */); 357 } 358 359 private void requestOrCancelSyncForEnabledProviders(boolean startSync) { 360 // sync everything that the user has enabled 361 int count = mActionFragment.getAdapter().getCount(); 362 for (int i = 0; i < count; i++) { 363 Action action = (Action) mActionFragment.getAdapter().getItem(i); 364 if (action.getIntent() == null) { 365 continue; 366 } 367 if (!action.isChecked()) { 368 continue; 369 } 370 Account account = action.getIntent().getParcelableExtra(EXTRA_ACCOUNT); 371 requestOrCancelSync(account, action.getKey(), startSync); 372 } 373 // plus whatever the system needs to sync, e.g., invisible sync adapters 374 if (mAccount != null) { 375 // Make a copy of these in case we update while calling this. 376 List<SyncAdapterType> invisibleAdapters = new ArrayList<SyncAdapterType>( 377 mInvisibleAdapters); 378 int size = invisibleAdapters.size(); 379 for (int index = 0; index < size; ++index) { 380 SyncAdapterType syncAdapter = invisibleAdapters.get(index); 381 // invisible sync adapters' account type should be same as current account type 382 if (syncAdapter.accountType.equals(mAccount.type)) { 383 requestOrCancelSync(mAccount, syncAdapter.authority, startSync); 384 } 385 } 386 } 387 } 388 389 private void requestOrCancelSync(Account account, String authority, boolean sync) { 390 if (sync) { 391 Bundle extras = new Bundle(); 392 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 393 ContentResolver.requestSync(account, authority, extras); 394 } else { 395 ContentResolver.cancelSync(account, authority); 396 } 397 } 398 399 private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) { 400 for (SyncInfo syncInfo : currentSyncs) { 401 if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) { 402 return true; 403 } 404 } 405 return false; 406 } 407 408 protected String formatSyncDate(Date date) { 409 return DateUtils.formatDateTime(this, date.getTime(), 410 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 411 } 412 413 private class LoadActionsTask extends AsyncTask<Account[], Void, ArrayList<Action>> { 414 415 @Override 416 protected ArrayList<Action> doInBackground(Account[]... params) { 417 return getActions(params[0]); 418 } 419 420 @Override 421 protected void onPostExecute(ArrayList<Action> result) { 422 // Set the icon based on whether sync is failing. 423 ContentFragment contentFragment = ((ContentFragment) getContentFragment()); 424 if (contentFragment != null) { 425 contentFragment.setIcon(mSyncIsFailing ? R.drawable.ic_settings_sync_error : 426 R.drawable.ic_settings_sync); 427 contentFragment.setDescriptionText(mSyncIsFailing ? 428 getString(R.string.sync_is_failing) : ""); 429 } 430 ((ActionAdapter) mActionFragment.getAdapter()).setActions(result); 431 } 432 } 433 434 private static final Comparator<Action> ADAPTER_COMPARATOR = new Comparator<Action>() { 435 436 @Override 437 public int compare(Action lhs, Action rhs) { 438 if (lhs == null && rhs == null) { 439 return 0; 440 } 441 if (lhs != null && rhs == null) { 442 return 1; 443 } 444 if (rhs != null && lhs == null) { 445 return -1; 446 } 447 return lhs.getTitle().compareTo(rhs.getTitle()); 448 } 449 }; 450} 451