Home | History | Annotate | Download | only in accounts
      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 
     17 package com.android.settings.accounts;
     18 
     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.preference.Preference;
     38 import android.preference.Preference.OnPreferenceClickListener;
     39 import android.preference.PreferenceScreen;
     40 import android.util.Log;
     41 import android.view.LayoutInflater;
     42 import android.view.Menu;
     43 import android.view.MenuInflater;
     44 import android.view.MenuItem;
     45 import android.view.View;
     46 import android.view.ViewGroup;
     47 import android.widget.ListView;
     48 import android.widget.TextView;
     49 
     50 import com.android.internal.logging.MetricsLogger;
     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 
     57 import java.util.ArrayList;
     58 import java.util.Date;
     59 import java.util.HashSet;
     60 import java.util.List;
     61 
     62 import static android.content.Intent.EXTRA_USER;
     63 
     64 /** Manages settings for Google Account. */
     65 public 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