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.support.v7.preference.Preference;
38import android.support.v7.preference.Preference.OnPreferenceClickListener;
39import android.support.v7.preference.PreferenceGroup;
40import android.support.v7.preference.PreferenceScreen;
41import android.util.Log;
42import android.view.LayoutInflater;
43import android.view.Menu;
44import android.view.MenuInflater;
45import android.view.MenuItem;
46import android.view.View;
47import android.view.ViewGroup;
48import android.widget.TextView;
49
50import com.android.internal.logging.MetricsProto.MetricsEvent;
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;
56import com.android.settingslib.accounts.AuthenticatorHelper;
57
58import java.util.ArrayList;
59import java.util.Date;
60import java.util.HashSet;
61import java.util.List;
62
63import static android.content.Intent.EXTRA_USER;
64
65/** Manages settings for Google Account. */
66public 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";
71
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";
76
77    private static final int MENU_SYNC_NOW_ID = Menu.FIRST;
78    private static final int MENU_SYNC_CANCEL_ID    = Menu.FIRST + 1;
79
80    private static final int REQUEST_SHOW_SYNC_SETTINGS = 1;
81
82    private String[] mAuthorities;
83    private TextView mErrorInfoView;
84
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;
90
91    @Override
92    protected int getMetricsCategory() {
93        return MetricsEvent.ACCOUNTS_MANAGE_ACCOUNTS;
94    }
95
96    @Override
97    public void onCreate(Bundle icicle) {
98        super.onCreate(icicle);
99
100        Bundle args = getArguments();
101        if (args != null && args.containsKey(KEY_ACCOUNT_TYPE)) {
102            mAccountType = args.getString(KEY_ACCOUNT_TYPE);
103        }
104        addPreferencesFromResource(R.xml.manage_accounts_settings);
105        setHasOptionsMenu(true);
106    }
107
108    @Override
109    public void onResume() {
110        super.onResume();
111        mAuthenticatorHelper.listenToAccountUpdates();
112        updateAuthDescriptions();
113        showAccountsIfNeeded();
114        showSyncState();
115    }
116
117    @Override
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);
125        return view;
126    }
127
128    @Override
129    public void onActivityCreated(Bundle savedInstanceState) {
130        super.onActivityCreated(savedInstanceState);
131
132        final Activity activity = getActivity();
133        final View view = getView();
134
135        mErrorInfoView = (TextView)view.findViewById(R.id.sync_settings_error_info);
136        mErrorInfoView.setVisibility(View.GONE);
137
138        mAuthorities = activity.getIntent().getStringArrayExtra(AUTHORITIES_FILTER_KEY);
139
140        Bundle args = getArguments();
141        if (args != null && args.containsKey(KEY_ACCOUNT_LABEL)) {
142            getActivity().setTitle(args.getString(KEY_ACCOUNT_LABEL));
143        }
144    }
145
146    @Override
147    public void onPause() {
148        super.onPause();
149        mAuthenticatorHelper.stopListeningToAccountUpdates();
150    }
151
152    @Override
153    public void onStop() {
154        super.onStop();
155        final Activity activity = getActivity();
156        activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM);
157        activity.getActionBar().setCustomView(null);
158    }
159
160    @Override
161    public boolean onPreferenceTreeClick(Preference preference) {
162        if (preference instanceof AccountPreference) {
163            startAccountSettings((AccountPreference) preference);
164        } else {
165            return false;
166        }
167        return true;
168    }
169
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);
178    }
179
180    @Override
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);
187    }
188
189    @Override
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);
196    }
197
198    @Override
199    public boolean onOptionsItemSelected(MenuItem item) {
200        switch (item.getItemId()) {
201        case MENU_SYNC_NOW_ID:
202            requestOrCancelSyncForAccounts(true);
203            return true;
204        case MENU_SYNC_CANCEL_ID:
205            requestOrCancelSyncForAccounts(false);
206            return true;
207        }
208        return super.onOptionsItemSelected(item);
209    }
210
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();
217        // For each account
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,
227                                    userId)) {
228                        if (sync) {
229                            ContentResolver.requestSyncAsUser(account, sa.authority, userId,
230                                    extras);
231                        } else {
232                            ContentResolver.cancelSyncAsUser(account, sa.authority, userId);
233                        }
234                    }
235                }
236            }
237        }
238    }
239
240    @Override
241    protected void onSyncStateUpdated() {
242        showSyncState();
243        // Catch any delayed delivery of update messages
244        final Activity activity = getActivity();
245        if (activity != null) {
246            activity.invalidateOptionsMenu();
247        }
248    }
249
250    /**
251     * Shows the sync state of the accounts. Note: it must be called after the accounts have been
252     * loaded, @see #showAccountsIfNeeded().
253     */
254    private void showSyncState() {
255        // Catch any delayed delivery of update messages
256        if (getActivity() == null || getActivity().isFinishing()) return;
257
258        final int userId = mUserHandle.getIdentifier();
259
260        // iterate over all the preferences, setting the state properly for each
261        List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
262
263        boolean anySyncFailed = false; // true if sync on any account failed
264        Date date = new Date();
265
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);
273            }
274        }
275        for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) {
276            Preference pref = getPreferenceScreen().getPreference(i);
277            if (! (pref instanceof AccountPreference)) {
278                continue;
279            }
280
281            AccountPreference accountPref = (AccountPreference) pref;
282            Account account = accountPref.getAccount();
283            int syncCount = 0;
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,
291                            userId);
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
296                            && syncEnabled
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;
303                    }
304                    syncingNow |= activelySyncing;
305                    if (status != null && lastSuccessTime < status.lastSuccessTime) {
306                        lastSuccessTime = status.lastSuccessTime;
307                    }
308                    syncCount += syncEnabled && userFacing.contains(authority) ? 1 : 0;
309                }
310            } else {
311                if (Log.isLoggable(TAG, Log.VERBOSE)) {
312                    Log.v(TAG, "no syncadapters found for " + account);
313                }
314            }
315            if (syncIsFailing) {
316                accountPref.setSyncStatus(AccountPreference.SYNC_ERROR, true);
317            } else if (syncCount == 0) {
318                accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
319            } else if (syncCount > 0) {
320                if (syncingNow) {
321                    accountPref.setSyncStatus(AccountPreference.SYNC_IN_PROGRESS, true);
322                } else {
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));
330                    }
331                }
332            } else {
333                accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
334            }
335        }
336
337        mErrorInfoView.setVisibility(anySyncFailed ? View.VISIBLE : View.GONE);
338    }
339
340
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)) {
346                return true;
347            }
348        }
349        return false;
350    }
351
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);
356    }
357
358    @Override
359    public void onAccountsUpdate(UserHandle userHandle) {
360        showAccountsIfNeeded();
361        onSyncStateUpdated();
362    }
363
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);
376
377            boolean showAccount = true;
378            if (mAuthorities != null && auths != null) {
379                showAccount = false;
380                for (String requestedAuthority : mAuthorities) {
381                    if (auths.contains(requestedAuthority)) {
382                        showAccount = true;
383                        break;
384                    }
385                }
386            }
387
388            if (showAccount) {
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;
395                }
396            }
397        }
398        if (mAccountType != null && mFirstAccount != null) {
399            addAuthenticatorSettings();
400        } else {
401            // There's no account, close activity
402            finish();
403        }
404    }
405
406    private void addAuthenticatorSettings() {
407        PreferenceScreen prefs = addPreferencesForType(mAccountType, getPreferenceScreen());
408        if (prefs != null) {
409            updatePreferenceIntents(prefs);
410        }
411    }
412
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;
418
419        /**
420         * @param className the class name of the fragment to be started.
421         * @param title the title resource id of the started preference panel.
422         */
423        public FragmentStarter(String className, int title) {
424            mClass = className;
425            mTitleRes = title;
426        }
427
428        @Override
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
433            // settings
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);
438            }
439            return true;
440        }
441    }
442
443    /**
444     * Recursively filters through the preference list provided by GoogleLoginService.
445     *
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.
448     */
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);
455            }
456            Intent intent = pref.getIntent();
457            if (intent != null) {
458                // Hack. Launch "Location" as fragment instead of as activity.
459                //
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.
465                //
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
475                    // will get fired.
476                    pref.setOnPreferenceClickListener(new FragmentStarter(
477                            LocationSettings.class.getName(),
478                            R.string.location_settings_title));
479                } else {
480                    ResolveInfo ri = pm.resolveActivityAsUser(intent,
481                            PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier());
482                    if (ri == null) {
483                        prefs.removePreference(pref);
484                        continue;
485                    } else {
486                        intent.putExtra(ACCOUNT_KEY, mFirstAccount);
487                        intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
488                        pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
489                            @Override
490                            public boolean onPreferenceClick(Preference preference) {
491                                Intent prefIntent = preference.getIntent();
492                                /*
493                                 * Check the intent to see if it resolves to a exported=false
494                                 * activity that doesn't share a uid with the authenticator.
495                                 *
496                                 * Otherwise the intent is considered unsafe in that it will be
497                                 * exploiting the fact that settings has system privileges.
498                                 */
499                                if (isSafeIntent(pm, prefIntent)) {
500                                    getActivity().startActivityAsUser(prefIntent, mUserHandle);
501                                } else {
502                                    Log.e(TAG,
503                                            "Refusing to launch authenticator intent because"
504                                                    + " it exploits Settings permissions: "
505                                                    + prefIntent);
506                                }
507                                return true;
508                            }
509                        });
510                    }
511                }
512            }
513            i++;
514        }
515    }
516
517    /**
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.
521     */
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) {
528            return false;
529        }
530        ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo;
531        ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo;
532        try {
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) {
538                    return true;
539                }
540            }
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);
545            return false;
546        }
547    }
548
549    @Override
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));
557            }
558        }
559    }
560}
561