2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.accounts;
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.accounts.AuthenticatorDescription;
22 import android.app.ActionBar;
23 import android.app.Activity;
24 import android.content.ContentResolver;
25 import android.content.Intent;
26 import android.content.SyncAdapterType;
27 import android.content.SyncInfo;
28 import android.content.SyncStatusInfo;
29 import android.content.pm.ActivityInfo;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.pm.ResolveInfo;
34 import android.graphics.drawable.Drawable;
35 import android.os.Bundle;
36 import android.os.UserHandle;
37 import android.support.v7.preference.Preference;
38 import android.support.v7.preference.Preference.OnPreferenceClickListener;
39 import android.support.v7.preference.PreferenceGroup;
40 import android.support.v7.preference.PreferenceScreen;
41 import android.util.Log;
42 import android.view.LayoutInflater;
43 import android.view.Menu;
44 import android.view.MenuInflater;
45 import android.view.MenuItem;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.widget.TextView;
50 import com.android.internal.logging.MetricsProto.MetricsEvent;
51 import com.android.settings.AccountPreference;
52 import com.android.settings.R;
53 import com.android.settings.SettingsActivity;
54 import com.android.settings.Utils;
55 import com.android.settings.location.LocationSettings;
56 import com.android.settingslib.accounts.AuthenticatorHelper;
58 import java.util.ArrayList;
59 import java.util.Date;
60 import java.util.HashSet;
61 import java.util.List;
63 import static android.content.Intent.EXTRA_USER;
65 /** Manages settings for Google Account. */
66 public class ManageAccountsSettings extends AccountPreferenceBase
67 implements AuthenticatorHelper.OnAccountsUpdateListener {
68 private static final String ACCOUNT_KEY = "account"; // to pass to auth settings
69 public static final String KEY_ACCOUNT_TYPE = "account_type";
70 public static final String KEY_ACCOUNT_LABEL = "account_label";
72 // Action name for the broadcast intent when the Google account preferences page is launching
73 // the location settings.
74 private static final String LAUNCHING_LOCATION_SETTINGS =
75 "com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS";
77 private static final int MENU_SYNC_NOW_ID = Menu.FIRST;
78 private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1;
80 private static final int REQUEST_SHOW_SYNC_SETTINGS = 1;
82 private String[] mAuthorities;
83 private TextView mErrorInfoView;
85 // If an account type is set, then show only accounts of that type
86 private String mAccountType;
87 // Temporary hack, to deal with backward compatibility
88 // mFirstAccount is used for the injected preferences
89 private Account mFirstAccount;
92 protected int getMetricsCategory() {
93 return MetricsEvent.ACCOUNTS_MANAGE_ACCOUNTS;
97 public void onCreate(Bundle icicle) {
98 super.onCreate(icicle);
100 Bundle args = getArguments();
101 if (args != null && args.containsKey(KEY_ACCOUNT_TYPE)) {
102 mAccountType = args.getString(KEY_ACCOUNT_TYPE);
104 addPreferencesFromResource(R.xml.manage_accounts_settings);
105 setHasOptionsMenu(true);
109 public void onResume() {
111 mAuthenticatorHelper.listenToAccountUpdates();
112 updateAuthDescriptions();
113 showAccountsIfNeeded();
118 public View onCreateView(LayoutInflater inflater, ViewGroup container,
119 Bundle savedInstanceState) {
120 final View view = inflater.inflate(R.layout.manage_accounts_screen, container, false);
121 final ViewGroup prefs_container = (ViewGroup) view.findViewById(R.id.prefs_container);
122 Utils.prepareCustomPreferencesList(container, view, prefs_container, false);
123 View prefs = super.onCreateView(inflater, prefs_container, savedInstanceState);
124 prefs_container.addView(prefs);
129 public void onActivityCreated(Bundle savedInstanceState) {
130 super.onActivityCreated(savedInstanceState);
132 final Activity activity = getActivity();
133 final View view = getView();
135 mErrorInfoView = (TextView)view.findViewById(R.id.sync_settings_error_info);
136 mErrorInfoView.setVisibility(View.GONE);
138 mAuthorities = activity.getIntent().getStringArrayExtra(AUTHORITIES_FILTER_KEY);
140 Bundle args = getArguments();
141 if (args != null && args.containsKey(KEY_ACCOUNT_LABEL)) {
142 getActivity().setTitle(args.getString(KEY_ACCOUNT_LABEL));
147 public void onPause() {
149 mAuthenticatorHelper.stopListeningToAccountUpdates();
153 public void onStop() {
155 final Activity activity = getActivity();
156 activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM);
157 activity.getActionBar().setCustomView(null);
161 public boolean onPreferenceTreeClick(Preference preference) {
162 if (preference instanceof AccountPreference) {
163 startAccountSettings((AccountPreference) preference);
170 private void startAccountSettings(AccountPreference acctPref) {
171 Bundle args = new Bundle();
172 args.putParcelable(AccountSyncSettings.ACCOUNT_KEY, acctPref.getAccount());
173 args.putParcelable(EXTRA_USER, mUserHandle);
174 ((SettingsActivity) getActivity()).startPreferencePanel(
175 AccountSyncSettings.class.getCanonicalName(), args,
176 R.string.account_sync_settings_title, acctPref.getAccount().name,
177 this, REQUEST_SHOW_SYNC_SETTINGS);
181 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
182 menu.add(0, MENU_SYNC_NOW_ID, 0, getString(R.string.sync_menu_sync_now))
183 .setIcon(R.drawable.ic_menu_refresh_holo_dark);
184 menu.add(0, MENU_SYNC_CANCEL_ID, 0, getString(R.string.sync_menu_sync_cancel))
185 .setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel);
186 super.onCreateOptionsMenu(menu, inflater);
190 public void onPrepareOptionsMenu(Menu menu) {
191 super.onPrepareOptionsMenu(menu);
192 boolean syncActive = !ContentResolver.getCurrentSyncsAsUser(
193 mUserHandle.getIdentifier()).isEmpty();
194 menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive);
195 menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive);
199 public boolean onOptionsItemSelected(MenuItem item) {
200 switch (item.getItemId()) {
201 case MENU_SYNC_NOW_ID:
202 requestOrCancelSyncForAccounts(true);
204 case MENU_SYNC_CANCEL_ID:
205 requestOrCancelSyncForAccounts(false);
208 return super.onOptionsItemSelected(item);
211 private void requestOrCancelSyncForAccounts(boolean sync) {
212 final int userId = mUserHandle.getIdentifier();
213 SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
214 Bundle extras = new Bundle();
215 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
216 int count = getPreferenceScreen().getPreferenceCount();
218 for (int i = 0; i < count; i++) {
219 Preference pref = getPreferenceScreen().getPreference(i);
220 if (pref instanceof AccountPreference) {
221 Account account = ((AccountPreference) pref).getAccount();
222 // For all available sync authorities, sync those that are enabled for the account
223 for (int j = 0; j < syncAdapters.length; j++) {
224 SyncAdapterType sa = syncAdapters[j];
225 if (syncAdapters[j].accountType.equals(mAccountType)
226 && ContentResolver.getSyncAutomaticallyAsUser(account, sa.authority,
229 ContentResolver.requestSyncAsUser(account, sa.authority, userId,
232 ContentResolver.cancelSyncAsUser(account, sa.authority, userId);
241 protected void onSyncStateUpdated() {
243 // Catch any delayed delivery of update messages
244 final Activity activity = getActivity();
245 if (activity != null) {
246 activity.invalidateOptionsMenu();
251 * Shows the sync state of the accounts. Note: it must be called after the accounts have been
252 * loaded, @see #showAccountsIfNeeded().
254 private void showSyncState() {
255 // Catch any delayed delivery of update messages
256 if (getActivity() == null || getActivity().isFinishing()) return;
258 final int userId = mUserHandle.getIdentifier();
260 // iterate over all the preferences, setting the state properly for each
261 List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
263 boolean anySyncFailed = false; // true if sync on any account failed
264 Date date = new Date();
266 // only track userfacing sync adapters when deciding if account is synced or not
267 final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
268 HashSet<String> userFacing = new HashSet<String>();
269 for (int k = 0, n = syncAdapters.length; k < n; k++) {
270 final SyncAdapterType sa = syncAdapters[k];
271 if (sa.isUserVisible()) {
272 userFacing.add(sa.authority);
275 for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) {
276 Preference pref = getPreferenceScreen().getPreference(i);
277 if (! (pref instanceof AccountPreference)) {
281 AccountPreference accountPref = (AccountPreference) pref;
282 Account account = accountPref.getAccount();
284 long lastSuccessTime = 0;
285 boolean syncIsFailing = false;
286 final ArrayList<String> authorities = accountPref.getAuthorities();
287 boolean syncingNow = false;
288 if (authorities != null) {
289 for (String authority : authorities) {
290 SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority,
292 boolean syncEnabled = isSyncEnabled(userId, account, authority);
293 boolean authorityIsPending = ContentResolver.isSyncPending(account, authority);
294 boolean activelySyncing = isSyncing(currentSyncs, account, authority);
295 boolean lastSyncFailed = status != null
297 && status.lastFailureTime != 0
298 && status.getLastFailureMesgAsInt(0)
299 != ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
300 if (lastSyncFailed && !activelySyncing && !authorityIsPending) {
301 syncIsFailing = true;
302 anySyncFailed = true;
304 syncingNow |= activelySyncing;
305 if (status != null && lastSuccessTime < status.lastSuccessTime) {
306 lastSuccessTime = status.lastSuccessTime;
308 syncCount += syncEnabled && userFacing.contains(authority) ? 1 : 0;
311 if (Log.isLoggable(TAG, Log.VERBOSE)) {
312 Log.v(TAG, "no syncadapters found for " + account);
316 accountPref.setSyncStatus(AccountPreference.SYNC_ERROR, true);
317 } else if (syncCount == 0) {
318 accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
319 } else if (syncCount > 0) {
321 accountPref.setSyncStatus(AccountPreference.SYNC_IN_PROGRESS, true);
323 accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, true);
324 if (lastSuccessTime > 0) {
325 accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, false);
326 date.setTime(lastSuccessTime);
327 final String timeString = formatSyncDate(date);
328 accountPref.setSummary(getResources().getString(
329 R.string.last_synced, timeString));
333 accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
337 mErrorInfoView.setVisibility(anySyncFailed ? View.VISIBLE : View.GONE);
341 private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) {
342 final int count = currentSyncs.size();
343 for (int i = 0; i < count; i++) {
344 SyncInfo syncInfo = currentSyncs.get(i);
345 if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
352 private boolean isSyncEnabled(int userId, Account account, String authority) {
353 return ContentResolver.getSyncAutomaticallyAsUser(account, authority, userId)
354 && ContentResolver.getMasterSyncAutomaticallyAsUser(userId)
355 && (ContentResolver.getIsSyncableAsUser(account, authority, userId) > 0);
359 public void onAccountsUpdate(UserHandle userHandle) {
360 showAccountsIfNeeded();
361 onSyncStateUpdated();
364 private void showAccountsIfNeeded() {
365 if (getActivity() == null) return;
366 Account[] accounts = AccountManager.get(getActivity()).getAccountsAsUser(
367 mUserHandle.getIdentifier());
368 getPreferenceScreen().removeAll();
369 mFirstAccount = null;
370 addPreferencesFromResource(R.xml.manage_accounts_settings);
371 for (int i = 0, n = accounts.length; i < n; i++) {
372 final Account account = accounts[i];
373 // If an account type is specified for this screen, skip other types
374 if (mAccountType != null && !account.type.equals(mAccountType)) continue;
375 final ArrayList<String> auths = getAuthoritiesForAccountType(account.type);
377 boolean showAccount = true;
378 if (mAuthorities != null && auths != null) {
380 for (String requestedAuthority : mAuthorities) {
381 if (auths.contains(requestedAuthority)) {
389 final Drawable icon = getDrawableForType(account.type);
390 final AccountPreference preference =
391 new AccountPreference(getPrefContext(), account, icon, auths, false);
392 getPreferenceScreen().addPreference(preference);
393 if (mFirstAccount == null) {
394 mFirstAccount = account;
398 if (mAccountType != null && mFirstAccount != null) {
399 addAuthenticatorSettings();
401 // There's no account, close activity
406 private void addAuthenticatorSettings() {
407 PreferenceScreen prefs = addPreferencesForType(mAccountType, getPreferenceScreen());
409 updatePreferenceIntents(prefs);
413 /** Listens to a preference click event and starts a fragment */
414 private class FragmentStarter
415 implements Preference.OnPreferenceClickListener {
416 private final String mClass;
417 private final int mTitleRes;
420 * @param className the class name of the fragment to be started.
421 * @param title the title resource id of the started preference panel.
423 public FragmentStarter(String className, int title) {
429 public boolean onPreferenceClick(Preference preference) {
430 ((SettingsActivity) getActivity()).startPreferencePanel(
431 mClass, null, mTitleRes, null, null, 0);
432 // Hack: announce that the Google account preferences page is launching the location
434 if (mClass.equals(LocationSettings.class.getName())) {
435 Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS);
436 getActivity().sendBroadcast(
437 intent, android.Manifest.permission.WRITE_SECURE_SETTINGS);
444 * Recursively filters through the preference list provided by GoogleLoginService.
446 * This method removes all the invalid intent from the list, adds account name as extra into the
447 * intent, and hack the location settings to start it as a fragment.
449 private void updatePreferenceIntents(PreferenceGroup prefs) {
450 final PackageManager pm = getActivity().getPackageManager();
451 for (int i = 0; i < prefs.getPreferenceCount();) {
452 Preference pref = prefs.getPreference(i);
453 if (pref instanceof PreferenceGroup) {
454 updatePreferenceIntents((PreferenceGroup) pref);
456 Intent intent = pref.getIntent();
457 if (intent != null) {
458 // Hack. Launch "Location" as fragment instead of as activity.
460 // When "Location" is launched as activity via Intent, there's no "Up" button at the
461 // top left, and if there's another running instance of "Location" activity, the
462 // back stack would usually point to some other place so the user won't be able to
463 // go back to the previous page by "back" key. Using fragment is a much easier
464 // solution to those problems.
466 // If we set Intent to null and assign a fragment to the PreferenceScreen item here,
467 // in order to make it work as expected, we still need to modify the container
468 // PreferenceActivity, override onPreferenceStartFragment() and call
469 // startPreferencePanel() there. In order to inject the title string there, more
470 // dirty further hack is still needed. It's much easier and cleaner to listen to
471 // preference click event here directly.
472 if (intent.getAction().equals(
473 android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) {
474 // The OnPreferenceClickListener overrides the click event completely. No intent
476 pref.setOnPreferenceClickListener(new FragmentStarter(
477 LocationSettings.class.getName(),
478 R.string.location_settings_title));
480 ResolveInfo ri = pm.resolveActivityAsUser(intent,
481 PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier());
483 prefs.removePreference(pref);
486 intent.putExtra(ACCOUNT_KEY, mFirstAccount);
487 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
488 pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
490 public boolean onPreferenceClick(Preference preference) {
491 Intent prefIntent = preference.getIntent();
493 * Check the intent to see if it resolves to a exported=false
494 * activity that doesn't share a uid with the authenticator.
496 * Otherwise the intent is considered unsafe in that it will be
497 * exploiting the fact that settings has system privileges.
499 if (isSafeIntent(pm, prefIntent)) {
500 getActivity().startActivityAsUser(prefIntent, mUserHandle);
503 "Refusing to launch authenticator intent because"
504 + " it exploits Settings permissions: "
518 * Determines if the supplied Intent is safe. A safe intent is one that is
519 * will launch a exported=true activity or owned by the same uid as the
520 * authenticator supplying the intent.
522 private boolean isSafeIntent(PackageManager pm, Intent intent) {
523 AuthenticatorDescription authDesc =
524 mAuthenticatorHelper.getAccountTypeDescription(mAccountType);
525 ResolveInfo resolveInfo =
526 pm.resolveActivityAsUser(intent, 0, mUserHandle.getIdentifier());
527 if (resolveInfo == null) {
530 ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo;
531 ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo;
533 if (resolvedActivityInfo.exported) {
534 if (resolvedActivityInfo.permission == null) {
535 return true; // exported activity without permission.
536 } else if (pm.checkPermission(resolvedActivityInfo.permission,
537 authDesc.packageName) == PackageManager.PERMISSION_GRANTED) {
541 ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0);
542 return resolvedAppInfo.uid == authenticatorAppInf.uid;
543 } catch (NameNotFoundException e) {
544 Log.e(TAG, "Intent considered unsafe due to exception.", e);
550 protected void onAuthDescriptionsUpdated() {
551 // Update account icons for all account preference items
552 for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
553 Preference pref = getPreferenceScreen().getPreference(i);
554 if (pref instanceof AccountPreference) {
555 AccountPreference accPref = (AccountPreference) pref;
556 accPref.setSummary(getLabelForType(accPref.getAccount().type));