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