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