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