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.AuthenticatorDescription; 22import android.app.ActionBar; 23import android.app.Activity; 24import android.content.ContentResolver; 25import android.content.Intent; 26import android.content.SyncAdapterType; 27import android.content.SyncInfo; 28import android.content.SyncStatusInfo; 29import android.content.pm.ActivityInfo; 30import android.content.pm.ApplicationInfo; 31import android.content.pm.PackageManager; 32import android.content.pm.PackageManager.NameNotFoundException; 33import android.content.pm.ResolveInfo; 34import android.graphics.drawable.Drawable; 35import android.os.Bundle; 36import android.os.UserHandle; 37import android.preference.Preference; 38import android.preference.Preference.OnPreferenceClickListener; 39import android.preference.PreferenceScreen; 40import android.util.Log; 41import android.view.LayoutInflater; 42import android.view.Menu; 43import android.view.MenuInflater; 44import android.view.MenuItem; 45import android.view.View; 46import android.view.ViewGroup; 47import android.widget.ListView; 48import android.widget.TextView; 49 50import com.android.internal.logging.MetricsLogger; 51import com.android.settings.AccountPreference; 52import com.android.settings.R; 53import com.android.settings.SettingsActivity; 54import com.android.settings.Utils; 55import com.android.settings.location.LocationSettings; 56 57import java.util.ArrayList; 58import java.util.Date; 59import java.util.HashSet; 60import java.util.List; 61 62import static android.content.Intent.EXTRA_USER; 63 64/** Manages settings for Google Account. */ 65public class ManageAccountsSettings extends AccountPreferenceBase 66 implements AuthenticatorHelper.OnAccountsUpdateListener { 67 private static final String ACCOUNT_KEY = "account"; // to pass to auth settings 68 public static final String KEY_ACCOUNT_TYPE = "account_type"; 69 public static final String KEY_ACCOUNT_LABEL = "account_label"; 70 71 // Action name for the broadcast intent when the Google account preferences page is launching 72 // the location settings. 73 private static final String LAUNCHING_LOCATION_SETTINGS = 74 "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS"; 75 76 private static final int MENU_SYNC_NOW_ID = Menu.FIRST; 77 private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1; 78 79 private static final int REQUEST_SHOW_SYNC_SETTINGS = 1; 80 81 private String[] mAuthorities; 82 private TextView mErrorInfoView; 83 84 // If an account type is set, then show only accounts of that type 85 private String mAccountType; 86 // Temporary hack, to deal with backward compatibility 87 // mFirstAccount is used for the injected preferences 88 private Account mFirstAccount; 89 90 @Override 91 protected int getMetricsCategory() { 92 return MetricsLogger.ACCOUNTS_MANAGE_ACCOUNTS; 93 } 94 95 @Override 96 public void onCreate(Bundle icicle) { 97 super.onCreate(icicle); 98 99 Bundle args = getArguments(); 100 if (args != null && args.containsKey(KEY_ACCOUNT_TYPE)) { 101 mAccountType = args.getString(KEY_ACCOUNT_TYPE); 102 } 103 addPreferencesFromResource(R.xml.manage_accounts_settings); 104 setHasOptionsMenu(true); 105 } 106 107 @Override 108 public void onResume() { 109 super.onResume(); 110 mAuthenticatorHelper.listenToAccountUpdates(); 111 updateAuthDescriptions(); 112 showAccountsIfNeeded(); 113 showSyncState(); 114 } 115 116 @Override 117 public View onCreateView(LayoutInflater inflater, ViewGroup container, 118 Bundle savedInstanceState) { 119 final View view = inflater.inflate(R.layout.manage_accounts_screen, container, false); 120 final ListView list = (ListView) view.findViewById(android.R.id.list); 121 Utils.prepareCustomPreferencesList(container, view, list, false); 122 return view; 123 } 124 125 @Override 126 public void onActivityCreated(Bundle savedInstanceState) { 127 super.onActivityCreated(savedInstanceState); 128 129 final Activity activity = getActivity(); 130 final View view = getView(); 131 132 mErrorInfoView = (TextView)view.findViewById(R.id.sync_settings_error_info); 133 mErrorInfoView.setVisibility(View.GONE); 134 135 mAuthorities = activity.getIntent().getStringArrayExtra(AUTHORITIES_FILTER_KEY); 136 137 Bundle args = getArguments(); 138 if (args != null && args.containsKey(KEY_ACCOUNT_LABEL)) { 139 getActivity().setTitle(args.getString(KEY_ACCOUNT_LABEL)); 140 } 141 } 142 143 @Override 144 public void onPause() { 145 super.onPause(); 146 mAuthenticatorHelper.stopListeningToAccountUpdates(); 147 } 148 149 @Override 150 public void onStop() { 151 super.onStop(); 152 final Activity activity = getActivity(); 153 activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM); 154 activity.getActionBar().setCustomView(null); 155 } 156 157 @Override 158 public boolean onPreferenceTreeClick(PreferenceScreen preferences, Preference preference) { 159 if (preference instanceof AccountPreference) { 160 startAccountSettings((AccountPreference) preference); 161 } else { 162 return false; 163 } 164 return true; 165 } 166 167 private void startAccountSettings(AccountPreference acctPref) { 168 Bundle args = new Bundle(); 169 args.putParcelable(AccountSyncSettings.ACCOUNT_KEY, acctPref.getAccount()); 170 args.putParcelable(EXTRA_USER, mUserHandle); 171 ((SettingsActivity) getActivity()).startPreferencePanel( 172 AccountSyncSettings.class.getCanonicalName(), args, 173 R.string.account_sync_settings_title, acctPref.getAccount().name, 174 this, REQUEST_SHOW_SYNC_SETTINGS); 175 } 176 177 @Override 178 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 179 menu.add(0, MENU_SYNC_NOW_ID, 0, getString(R.string.sync_menu_sync_now)) 180 .setIcon(R.drawable.ic_menu_refresh_holo_dark); 181 menu.add(0, MENU_SYNC_CANCEL_ID, 0, getString(R.string.sync_menu_sync_cancel)) 182 .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel); 183 super.onCreateOptionsMenu(menu, inflater); 184 } 185 186 @Override 187 public void onPrepareOptionsMenu(Menu menu) { 188 super.onPrepareOptionsMenu(menu); 189 boolean syncActive = !ContentResolver.getCurrentSyncsAsUser( 190 mUserHandle.getIdentifier()).isEmpty(); 191 menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive); 192 menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive); 193 } 194 195 @Override 196 public boolean onOptionsItemSelected(MenuItem item) { 197 switch (item.getItemId()) { 198 case MENU_SYNC_NOW_ID: 199 requestOrCancelSyncForAccounts(true); 200 return true; 201 case MENU_SYNC_CANCEL_ID: 202 requestOrCancelSyncForAccounts(false); 203 return true; 204 } 205 return super.onOptionsItemSelected(item); 206 } 207 208 private void requestOrCancelSyncForAccounts(boolean sync) { 209 final int userId = mUserHandle.getIdentifier(); 210 SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId); 211 Bundle extras = new Bundle(); 212 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); 213 int count = getPreferenceScreen().getPreferenceCount(); 214 // For each account 215 for (int i = 0; i < count; i++) { 216 Preference pref = getPreferenceScreen().getPreference(i); 217 if (pref instanceof AccountPreference) { 218 Account account = ((AccountPreference) pref).getAccount(); 219 // For all available sync authorities, sync those that are enabled for the account 220 for (int j = 0; j < syncAdapters.length; j++) { 221 SyncAdapterType sa = syncAdapters[j]; 222 if (syncAdapters[j].accountType.equals(mAccountType) 223 && ContentResolver.getSyncAutomaticallyAsUser(account, sa.authority, 224 userId)) { 225 if (sync) { 226 ContentResolver.requestSyncAsUser(account, sa.authority, userId, 227 extras); 228 } else { 229 ContentResolver.cancelSyncAsUser(account, sa.authority, userId); 230 } 231 } 232 } 233 } 234 } 235 } 236 237 @Override 238 protected void onSyncStateUpdated() { 239 showSyncState(); 240 // Catch any delayed delivery of update messages 241 final Activity activity = getActivity(); 242 if (activity != null) { 243 activity.invalidateOptionsMenu(); 244 } 245 } 246 247 /** 248 * Shows the sync state of the accounts. Note: it must be called after the accounts have been 249 * loaded, @see #showAccountsIfNeeded(). 250 */ 251 private void showSyncState() { 252 // Catch any delayed delivery of update messages 253 if (getActivity() == null || getActivity().isFinishing()) return; 254 255 final int userId = mUserHandle.getIdentifier(); 256 257 // iterate over all the preferences, setting the state properly for each 258 List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId); 259 260 boolean anySyncFailed = false; // true if sync on any account failed 261 Date date = new Date(); 262 263 // only track userfacing sync adapters when deciding if account is synced or not 264 final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId); 265 HashSet<String> userFacing = new HashSet<String>(); 266 for (int k = 0, n = syncAdapters.length; k < n; k++) { 267 final SyncAdapterType sa = syncAdapters[k]; 268 if (sa.isUserVisible()) { 269 userFacing.add(sa.authority); 270 } 271 } 272 for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) { 273 Preference pref = getPreferenceScreen().getPreference(i); 274 if (! (pref instanceof AccountPreference)) { 275 continue; 276 } 277 278 AccountPreference accountPref = (AccountPreference) pref; 279 Account account = accountPref.getAccount(); 280 int syncCount = 0; 281 long lastSuccessTime = 0; 282 boolean syncIsFailing = false; 283 final ArrayList<String> authorities = accountPref.getAuthorities(); 284 boolean syncingNow = false; 285 if (authorities != null) { 286 for (String authority : authorities) { 287 SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority, 288 userId); 289 boolean syncEnabled = isSyncEnabled(userId, account, authority); 290 boolean authorityIsPending = ContentResolver.isSyncPending(account, authority); 291 boolean activelySyncing = isSyncing(currentSyncs, account, authority); 292 boolean lastSyncFailed = status != null 293 && syncEnabled 294 && status.lastFailureTime != 0 295 && status.getLastFailureMesgAsInt(0) 296 != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; 297 if (lastSyncFailed && !activelySyncing && !authorityIsPending) { 298 syncIsFailing = true; 299 anySyncFailed = true; 300 } 301 syncingNow |= activelySyncing; 302 if (status != null && lastSuccessTime < status.lastSuccessTime) { 303 lastSuccessTime = status.lastSuccessTime; 304 } 305 syncCount += syncEnabled && userFacing.contains(authority) ? 1 : 0; 306 } 307 } else { 308 if (Log.isLoggable(TAG, Log.VERBOSE)) { 309 Log.v(TAG, "no syncadapters found for " + account); 310 } 311 } 312 if (syncIsFailing) { 313 accountPref.setSyncStatus(AccountPreference.SYNC_ERROR, true); 314 } else if (syncCount == 0) { 315 accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true); 316 } else if (syncCount > 0) { 317 if (syncingNow) { 318 accountPref.setSyncStatus(AccountPreference.SYNC_IN_PROGRESS, true); 319 } else { 320 accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, true); 321 if (lastSuccessTime > 0) { 322 accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, false); 323 date.setTime(lastSuccessTime); 324 final String timeString = formatSyncDate(date); 325 accountPref.setSummary(getResources().getString( 326 R.string.last_synced, timeString)); 327 } 328 } 329 } else { 330 accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true); 331 } 332 } 333 334 mErrorInfoView.setVisibility(anySyncFailed ? View.VISIBLE : View.GONE); 335 } 336 337 338 private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) { 339 final int count = currentSyncs.size(); 340 for (int i = 0; i < count; i++) { 341 SyncInfo syncInfo = currentSyncs.get(i); 342 if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) { 343 return true; 344 } 345 } 346 return false; 347 } 348 349 private boolean isSyncEnabled(int userId, Account account, String authority) { 350 return ContentResolver.getSyncAutomaticallyAsUser(account, authority, userId) 351 && ContentResolver.getMasterSyncAutomaticallyAsUser(userId) 352 && (ContentResolver.getIsSyncableAsUser(account, authority, userId) > 0); 353 } 354 355 @Override 356 public void onAccountsUpdate(UserHandle userHandle) { 357 showAccountsIfNeeded(); 358 onSyncStateUpdated(); 359 } 360 361 private void showAccountsIfNeeded() { 362 if (getActivity() == null) return; 363 Account[] accounts = AccountManager.get(getActivity()).getAccountsAsUser( 364 mUserHandle.getIdentifier()); 365 getPreferenceScreen().removeAll(); 366 mFirstAccount = null; 367 addPreferencesFromResource(R.xml.manage_accounts_settings); 368 for (int i = 0, n = accounts.length; i < n; i++) { 369 final Account account = accounts[i]; 370 // If an account type is specified for this screen, skip other types 371 if (mAccountType != null && !account.type.equals(mAccountType)) continue; 372 final ArrayList<String> auths = getAuthoritiesForAccountType(account.type); 373 374 boolean showAccount = true; 375 if (mAuthorities != null && auths != null) { 376 showAccount = false; 377 for (String requestedAuthority : mAuthorities) { 378 if (auths.contains(requestedAuthority)) { 379 showAccount = true; 380 break; 381 } 382 } 383 } 384 385 if (showAccount) { 386 final Drawable icon = getDrawableForType(account.type); 387 final AccountPreference preference = 388 new AccountPreference(getActivity(), account, icon, auths, false); 389 getPreferenceScreen().addPreference(preference); 390 if (mFirstAccount == null) { 391 mFirstAccount = account; 392 } 393 } 394 } 395 if (mAccountType != null && mFirstAccount != null) { 396 addAuthenticatorSettings(); 397 } else { 398 // There's no account, close activity 399 finish(); 400 } 401 } 402 403 private void addAuthenticatorSettings() { 404 PreferenceScreen prefs = addPreferencesForType(mAccountType, getPreferenceScreen()); 405 if (prefs != null) { 406 updatePreferenceIntents(prefs); 407 } 408 } 409 410 /** Listens to a preference click event and starts a fragment */ 411 private class FragmentStarter 412 implements Preference.OnPreferenceClickListener { 413 private final String mClass; 414 private final int mTitleRes; 415 416 /** 417 * @param className the class name of the fragment to be started. 418 * @param title the title resource id of the started preference panel. 419 */ 420 public FragmentStarter(String className, int title) { 421 mClass = className; 422 mTitleRes = title; 423 } 424 425 @Override 426 public boolean onPreferenceClick(Preference preference) { 427 ((SettingsActivity) getActivity()).startPreferencePanel( 428 mClass, null, mTitleRes, null, null, 0); 429 // Hack: announce that the Google account preferences page is launching the location 430 // settings 431 if (mClass.equals(LocationSettings.class.getName())) { 432 Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS); 433 getActivity().sendBroadcast( 434 intent, android.Manifest.permission.WRITE_SECURE_SETTINGS); 435 } 436 return true; 437 } 438 } 439 440 /** 441 * Filters through the preference list provided by GoogleLoginService. 442 * 443 * This method removes all the invalid intent from the list, adds account name as extra into the 444 * intent, and hack the location settings to start it as a fragment. 445 */ 446 private void updatePreferenceIntents(PreferenceScreen prefs) { 447 final PackageManager pm = getActivity().getPackageManager(); 448 for (int i = 0; i < prefs.getPreferenceCount();) { 449 Preference pref = prefs.getPreference(i); 450 Intent intent = pref.getIntent(); 451 if (intent != null) { 452 // Hack. Launch "Location" as fragment instead of as activity. 453 // 454 // When "Location" is launched as activity via Intent, there's no "Up" button at the 455 // top left, and if there's another running instance of "Location" activity, the 456 // back stack would usually point to some other place so the user won't be able to 457 // go back to the previous page by "back" key. Using fragment is a much easier 458 // solution to those problems. 459 // 460 // If we set Intent to null and assign a fragment to the PreferenceScreen item here, 461 // in order to make it work as expected, we still need to modify the container 462 // PreferenceActivity, override onPreferenceStartFragment() and call 463 // startPreferencePanel() there. In order to inject the title string there, more 464 // dirty further hack is still needed. It's much easier and cleaner to listen to 465 // preference click event here directly. 466 if (intent.getAction().equals( 467 android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) { 468 // The OnPreferenceClickListener overrides the click event completely. No intent 469 // will get fired. 470 pref.setOnPreferenceClickListener(new FragmentStarter( 471 LocationSettings.class.getName(), 472 R.string.location_settings_title)); 473 } else { 474 ResolveInfo ri = pm.resolveActivityAsUser(intent, 475 PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier()); 476 if (ri == null) { 477 prefs.removePreference(pref); 478 continue; 479 } else { 480 intent.putExtra(ACCOUNT_KEY, mFirstAccount); 481 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); 482 pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { 483 @Override 484 public boolean onPreferenceClick(Preference preference) { 485 Intent prefIntent = preference.getIntent(); 486 /* 487 * Check the intent to see if it resolves to a exported=false 488 * activity that doesn't share a uid with the authenticator. 489 * 490 * Otherwise the intent is considered unsafe in that it will be 491 * exploiting the fact that settings has system privileges. 492 */ 493 if (isSafeIntent(pm, prefIntent)) { 494 getActivity().startActivityAsUser(prefIntent, mUserHandle); 495 } else { 496 Log.e(TAG, 497 "Refusing to launch authenticator intent because" 498 + "it exploits Settings permissions: " 499 + prefIntent); 500 } 501 return true; 502 } 503 }); 504 } 505 } 506 } 507 i++; 508 } 509 } 510 511 /** 512 * Determines if the supplied Intent is safe. A safe intent is one that is 513 * will launch a exported=true activity or owned by the same uid as the 514 * authenticator supplying the intent. 515 */ 516 private boolean isSafeIntent(PackageManager pm, Intent intent) { 517 AuthenticatorDescription authDesc = 518 mAuthenticatorHelper.getAccountTypeDescription(mAccountType); 519 ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); 520 if (resolveInfo == null) { 521 return false; 522 } 523 ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo; 524 ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo; 525 try { 526 ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0); 527 return resolvedActivityInfo.exported 528 || resolvedAppInfo.uid == authenticatorAppInf.uid; 529 } catch (NameNotFoundException e) { 530 Log.e(TAG, 531 "Intent considered unsafe due to exception.", 532 e); 533 return false; 534 } 535 } 536 537 @Override 538 protected void onAuthDescriptionsUpdated() { 539 // Update account icons for all account preference items 540 for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) { 541 Preference pref = getPreferenceScreen().getPreference(i); 542 if (pref instanceof AccountPreference) { 543 AccountPreference accPref = (AccountPreference) pref; 544 accPref.setSummary(getLabelForType(accPref.getAccount().type)); 545 } 546 } 547 } 548} 549