1/* 2 * Copyright (C) 2015 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 android.accounts.Account; 20import android.accounts.AccountManager; 21import android.app.Activity; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.SyncAdapterType; 26import android.content.SyncInfo; 27import android.content.SyncStatusInfo; 28import android.content.SyncStatusObserver; 29import android.content.pm.PackageManager; 30import android.content.pm.ProviderInfo; 31import android.os.Bundle; 32import android.os.Handler; 33import android.os.UserHandle; 34import android.support.v17.preference.LeanbackPreferenceFragment; 35import android.support.v7.preference.Preference; 36import android.support.v7.preference.PreferenceGroup; 37import android.text.TextUtils; 38import android.text.format.DateUtils; 39import android.util.Log; 40 41import com.android.settingslib.accounts.AuthenticatorHelper; 42import com.android.tv.settings.R; 43 44import com.google.android.collect.Lists; 45 46import java.util.ArrayList; 47import java.util.Collections; 48import java.util.List; 49 50public class AccountSyncFragment extends LeanbackPreferenceFragment implements 51 AuthenticatorHelper.OnAccountsUpdateListener { 52 private static final String TAG = "AccountSyncFragment"; 53 54 private static final String ARG_ACCOUNT = "account"; 55 private static final String KEY_REMOVE_ACCOUNT = "remove_account"; 56 private static final String KEY_SYNC_NOW = "sync_now"; 57 private static final String KEY_SYNC_ADAPTERS = "sync_adapters"; 58 59 private Object mStatusChangeListenerHandle; 60 private UserHandle mUserHandle; 61 private Account mAccount; 62 private ArrayList<SyncAdapterType> mInvisibleAdapters = Lists.newArrayList(); 63 64 private PreferenceGroup mSyncCategory; 65 66 private final Handler mHandler = new Handler(); 67 private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() { 68 public void onStatusChanged(int which) { 69 mHandler.post(new Runnable() { 70 public void run() { 71 if (isResumed()) { 72 onSyncStateUpdated(); 73 } 74 } 75 }); 76 } 77 }; 78 private AuthenticatorHelper mAuthenticatorHelper; 79 80 public static AccountSyncFragment newInstance(Account account) { 81 final Bundle b = new Bundle(1); 82 prepareArgs(b, account); 83 final AccountSyncFragment f = new AccountSyncFragment(); 84 f.setArguments(b); 85 return f; 86 } 87 88 public static void prepareArgs(Bundle b, Account account) { 89 b.putParcelable(ARG_ACCOUNT, account); 90 } 91 92 @Override 93 public void onCreate(Bundle savedInstanceState) { 94 mUserHandle = new UserHandle(UserHandle.myUserId()); 95 mAccount = getArguments().getParcelable(ARG_ACCOUNT); 96 mAuthenticatorHelper = new AuthenticatorHelper(getActivity(), mUserHandle, this); 97 98 super.onCreate(savedInstanceState); 99 100 if (Log.isLoggable(TAG, Log.VERBOSE)) { 101 Log.v(TAG, "Got account: " + mAccount); 102 } 103 } 104 105 @Override 106 public void onStart() { 107 super.onStart(); 108 mStatusChangeListenerHandle = ContentResolver.addStatusChangeListener( 109 ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE 110 | ContentResolver.SYNC_OBSERVER_TYPE_STATUS 111 | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, 112 mSyncStatusObserver); 113 onSyncStateUpdated(); 114 mAuthenticatorHelper.listenToAccountUpdates(); 115 mAuthenticatorHelper.updateAuthDescriptions(getActivity()); 116 } 117 118 @Override 119 public void onResume() { 120 super.onResume(); 121 mHandler.post(() -> onAccountsUpdate(mUserHandle)); 122 } 123 124 @Override 125 public void onStop() { 126 super.onStop(); 127 ContentResolver.removeStatusChangeListener(mStatusChangeListenerHandle); 128 mAuthenticatorHelper.stopListeningToAccountUpdates(); 129 } 130 131 @Override 132 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 133 setPreferencesFromResource(R.xml.account_preference, null); 134 135 getPreferenceScreen().setTitle(mAccount.name); 136 137 final Preference removeAccountPref = findPreference(KEY_REMOVE_ACCOUNT); 138 removeAccountPref.setIntent(new Intent(getActivity(), RemoveAccountDialog.class) 139 .putExtra(AccountSyncActivity.EXTRA_ACCOUNT, mAccount.name)); 140 141 mSyncCategory = (PreferenceGroup) findPreference(KEY_SYNC_ADAPTERS); 142 } 143 144 @Override 145 public boolean onPreferenceTreeClick(Preference preference) { 146 if (preference instanceof SyncStateSwitchPreference) { 147 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) preference; 148 String authority = syncPref.getAuthority(); 149 Account account = syncPref.getAccount(); 150 final int userId = mUserHandle.getIdentifier(); 151 if (syncPref.isOneTimeSyncMode()) { 152 requestOrCancelSync(account, authority, true); 153 } else { 154 boolean syncOn = syncPref.isChecked(); 155 boolean oldSyncState = ContentResolver.getSyncAutomaticallyAsUser(account, 156 authority, userId); 157 if (syncOn != oldSyncState) { 158 // if we're enabling sync, this will request a sync as well 159 ContentResolver.setSyncAutomaticallyAsUser(account, authority, syncOn, userId); 160 // if the master sync switch is off, the request above will 161 // get dropped. when the user clicks on this toggle, 162 // we want to force the sync, however. 163 if (!ContentResolver.getMasterSyncAutomaticallyAsUser(userId) || !syncOn) { 164 requestOrCancelSync(account, authority, syncOn); 165 } 166 } 167 } 168 return true; 169 } else if (TextUtils.equals(preference.getKey(), KEY_SYNC_NOW)) { 170 boolean syncActive = !ContentResolver.getCurrentSyncsAsUser( 171 mUserHandle.getIdentifier()).isEmpty(); 172 if (syncActive) { 173 cancelSyncForEnabledProviders(); 174 } else { 175 startSyncForEnabledProviders(); 176 } 177 return true; 178 } else { 179 return super.onPreferenceTreeClick(preference); 180 } 181 } 182 183 private void startSyncForEnabledProviders() { 184 requestOrCancelSyncForEnabledProviders(true /* start them */); 185 final Activity activity = getActivity(); 186 if (activity != null) { 187 activity.invalidateOptionsMenu(); 188 } 189 } 190 191 private void cancelSyncForEnabledProviders() { 192 requestOrCancelSyncForEnabledProviders(false /* cancel them */); 193 final Activity activity = getActivity(); 194 if (activity != null) { 195 activity.invalidateOptionsMenu(); 196 } 197 } 198 199 private void requestOrCancelSyncForEnabledProviders(boolean startSync) { 200 // sync everything that the user has enabled 201 int count = mSyncCategory.getPreferenceCount(); 202 for (int i = 0; i < count; i++) { 203 Preference pref = mSyncCategory.getPreference(i); 204 if (! (pref instanceof SyncStateSwitchPreference)) { 205 continue; 206 } 207 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) pref; 208 if (!syncPref.isChecked()) { 209 continue; 210 } 211 requestOrCancelSync(syncPref.getAccount(), syncPref.getAuthority(), startSync); 212 } 213 // plus whatever the system needs to sync, e.g., invisible sync adapters 214 if (mAccount != null) { 215 for (SyncAdapterType syncAdapter : mInvisibleAdapters) { 216 requestOrCancelSync(mAccount, syncAdapter.authority, startSync); 217 } 218 } 219 } 220 221 private void requestOrCancelSync(Account account, String authority, boolean flag) { 222 if (flag) { 223 Bundle extras = new Bundle(); 224 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 225 ContentResolver.requestSyncAsUser(account, authority, mUserHandle.getIdentifier(), 226 extras); 227 } else { 228 ContentResolver.cancelSyncAsUser(account, authority, mUserHandle.getIdentifier()); 229 } 230 } 231 232 private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) { 233 for (SyncInfo syncInfo : currentSyncs) { 234 if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) { 235 return true; 236 } 237 } 238 return false; 239 } 240 241 private boolean accountExists(Account account) { 242 if (account == null) return false; 243 244 Account[] accounts = AccountManager.get(getActivity()).getAccountsByTypeAsUser( 245 account.type, mUserHandle); 246 for (final Account other : accounts) { 247 if (other.equals(account)) { 248 return true; 249 } 250 } 251 return false; 252 } 253 254 @Override 255 public void onAccountsUpdate(UserHandle userHandle) { 256 if (!isResumed()) { 257 return; 258 } 259 if (!accountExists(mAccount)) { 260 // The account was deleted 261 if (!getFragmentManager().popBackStackImmediate()) { 262 getActivity().finish(); 263 } 264 return; 265 } 266 updateAccountSwitches(); 267 onSyncStateUpdated(); 268 } 269 270 private void onSyncStateUpdated() { 271 // iterate over all the preferences, setting the state properly for each 272 final int userId = mUserHandle.getIdentifier(); 273 List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId); 274// boolean syncIsFailing = false; 275 276 // Refresh the sync status switches - some syncs may have become active. 277 updateAccountSwitches(); 278 279 for (int i = 0, count = mSyncCategory.getPreferenceCount(); i < count; i++) { 280 Preference pref = mSyncCategory.getPreference(i); 281 if (! (pref instanceof SyncStateSwitchPreference)) { 282 continue; 283 } 284 SyncStateSwitchPreference syncPref = (SyncStateSwitchPreference) pref; 285 286 String authority = syncPref.getAuthority(); 287 Account account = syncPref.getAccount(); 288 289 SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority, userId); 290 boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser(account, authority, 291 userId); 292 boolean authorityIsPending = status != null && status.pending; 293 boolean initialSync = status != null && status.initialize; 294 295 boolean activelySyncing = isSyncing(currentSyncs, account, authority); 296 boolean lastSyncFailed = status != null 297 && status.lastFailureTime != 0 298 && status.getLastFailureMesgAsInt(0) 299 != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; 300 if (!syncEnabled) lastSyncFailed = false; 301// if (lastSyncFailed && !activelySyncing && !authorityIsPending) { 302// syncIsFailing = true; 303// } 304 if (Log.isLoggable(TAG, Log.VERBOSE)) { 305 Log.v(TAG, "Update sync status: " + account + " " + authority + 306 " active = " + activelySyncing + " pend =" + authorityIsPending); 307 } 308 309 final long successEndTime = (status == null) ? 0 : status.lastSuccessTime; 310 if (!syncEnabled) { 311 syncPref.setSummary(R.string.sync_disabled); 312 } else if (activelySyncing) { 313 syncPref.setSummary(R.string.sync_in_progress); 314 } else if (successEndTime != 0) { 315 final String timeString = DateUtils.formatDateTime(getActivity(), successEndTime, 316 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 317 syncPref.setSummary(getResources().getString(R.string.last_synced, timeString)); 318 } else { 319 syncPref.setSummary(""); 320 } 321 int syncState = ContentResolver.getIsSyncableAsUser(account, authority, userId); 322 323 syncPref.setActive(activelySyncing && (syncState >= 0) && 324 !initialSync); 325 syncPref.setPending(authorityIsPending && (syncState >= 0) && 326 !initialSync); 327 328 syncPref.setFailed(lastSyncFailed); 329 final boolean oneTimeSyncMode = !ContentResolver.getMasterSyncAutomaticallyAsUser( 330 userId); 331 syncPref.setOneTimeSyncMode(oneTimeSyncMode); 332 syncPref.setChecked(oneTimeSyncMode || syncEnabled); 333 } 334 } 335 336 private void updateAccountSwitches() { 337 mInvisibleAdapters.clear(); 338 339 SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser( 340 mUserHandle.getIdentifier()); 341 ArrayList<String> authorities = new ArrayList<>(syncAdapters.length); 342 for (SyncAdapterType sa : syncAdapters) { 343 // Only keep track of sync adapters for this account 344 if (!sa.accountType.equals(mAccount.type)) continue; 345 if (sa.isUserVisible()) { 346 if (Log.isLoggable(TAG, Log.VERBOSE)) { 347 Log.v(TAG, "updateAccountSwitches: added authority " + sa.authority 348 + " to accountType " + sa.accountType); 349 } 350 authorities.add(sa.authority); 351 } else { 352 // keep track of invisible sync adapters, so sync now forces 353 // them to sync as well. 354 mInvisibleAdapters.add(sa); 355 } 356 } 357 358 mSyncCategory.removeAll(); 359 final List<Preference> switches = new ArrayList<>(authorities.size()); 360 361 if (Log.isLoggable(TAG, Log.VERBOSE)) { 362 Log.v(TAG, "looking for sync adapters that match account " + mAccount); 363 } 364 for (final String authority : authorities) { 365 // We could check services here.... 366 int syncState = ContentResolver.getIsSyncableAsUser(mAccount, authority, 367 mUserHandle.getIdentifier()); 368 if (Log.isLoggable(TAG, Log.VERBOSE)) { 369 Log.v(TAG, " found authority " + authority + " " + syncState); 370 } 371 if (syncState > 0) { 372 final Preference pref = createSyncStateSwitch(mAccount, authority); 373 switches.add(pref); 374 } 375 } 376 377 Collections.sort(switches); 378 for (final Preference pref : switches) { 379 mSyncCategory.addPreference(pref); 380 } 381 } 382 383 private Preference createSyncStateSwitch(Account account, String authority) { 384 final Context themedContext = getPreferenceManager().getContext(); 385 SyncStateSwitchPreference preference = 386 new SyncStateSwitchPreference(themedContext, account, authority); 387 preference.setPersistent(false); 388 final PackageManager packageManager = getActivity().getPackageManager(); 389 final ProviderInfo providerInfo = packageManager.resolveContentProviderAsUser( 390 authority, 0, mUserHandle.getIdentifier()); 391 if (providerInfo == null) { 392 return null; 393 } 394 CharSequence providerLabel = providerInfo.loadLabel(packageManager); 395 if (TextUtils.isEmpty(providerLabel)) { 396 Log.e(TAG, "Provider needs a label for authority '" + authority + "'"); 397 return null; 398 } 399 String title = getString(R.string.sync_item_title, providerLabel); 400 preference.setTitle(title); 401 preference.setKey(authority); 402 return preference; 403 } 404 405} 406