1/*
2 * Copyright (C) 2014 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
19
20import android.accounts.Account;
21import android.accounts.AccountManager;
22import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
25import android.app.DialogFragment;
26import android.content.BroadcastReceiver;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.DialogInterface;
30import android.content.Intent;
31import android.content.IntentFilter;
32import android.content.pm.UserInfo;
33import android.graphics.drawable.Drawable;
34import android.os.Bundle;
35import android.os.UserHandle;
36import android.os.UserManager;
37import android.os.Process;
38import android.util.Log;
39import android.util.SparseArray;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.preference.Preference;
44import android.preference.Preference.OnPreferenceClickListener;
45import android.preference.PreferenceGroup;
46import android.preference.PreferenceCategory;
47import android.preference.PreferenceScreen;
48
49import com.android.settings.R;
50import com.android.settings.SettingsPreferenceFragment;
51import com.android.settings.Utils;
52
53import java.util.ArrayList;
54import java.util.Collections;
55import java.util.Comparator;
56import java.util.List;
57
58import static android.content.Intent.EXTRA_USER;
59import static android.os.UserManager.DISALLOW_MODIFY_ACCOUNTS;
60import static android.provider.Settings.EXTRA_AUTHORITIES;
61
62/**
63 * Settings screen for the account types on the device.
64 * This shows all account types available for personal and work profiles.
65 *
66 * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for
67 * which the action needs to be performed is different to the one the Settings App will run in.
68 */
69public class AccountSettings extends SettingsPreferenceFragment
70        implements AuthenticatorHelper.OnAccountsUpdateListener,
71        OnPreferenceClickListener {
72    public static final String TAG = "AccountSettings";
73
74    private static final String KEY_ACCOUNT = "account";
75
76    private static final String ADD_ACCOUNT_ACTION = "android.settings.ADD_ACCOUNT_SETTINGS";
77    private static final String TAG_CONFIRM_AUTO_SYNC_CHANGE = "confirmAutoSyncChange";
78
79    private static final int ORDER_LAST = 1001;
80    private static final int ORDER_NEXT_TO_LAST = 1000;
81
82    private UserManager mUm;
83    private SparseArray<ProfileData> mProfiles = new SparseArray<ProfileData>();
84    private ManagedProfileBroadcastReceiver mManagedProfileBroadcastReceiver
85                = new ManagedProfileBroadcastReceiver();
86    private Preference mProfileNotAvailablePreference;
87    private String[] mAuthorities;
88    private int mAuthoritiesCount = 0;
89
90    /**
91     * Holds data related to the accounts belonging to one profile.
92     */
93    private static class ProfileData {
94        /**
95         * The preference that displays the accounts.
96         */
97        public PreferenceGroup preferenceGroup;
98        /**
99         * The preference that displays the add account button.
100         */
101        public Preference addAccountPreference;
102        /**
103         * The preference that displays the button to remove the managed profile
104         */
105        public Preference removeWorkProfilePreference;
106        /**
107         * The {@link AuthenticatorHelper} that holds accounts data for this profile.
108         */
109        public AuthenticatorHelper authenticatorHelper;
110        /**
111         * The {@link UserInfo} of the profile.
112         */
113        public UserInfo userInfo;
114    }
115
116    @Override
117    public void onCreate(Bundle savedInstanceState) {
118        super.onCreate(savedInstanceState);
119        mUm = (UserManager) getSystemService(Context.USER_SERVICE);
120        mProfileNotAvailablePreference = new Preference(getActivity());
121        mAuthorities = getActivity().getIntent().getStringArrayExtra(EXTRA_AUTHORITIES);
122        if (mAuthorities != null) {
123            mAuthoritiesCount = mAuthorities.length;
124        }
125        setHasOptionsMenu(true);
126    }
127
128    @Override
129    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
130        inflater.inflate(R.menu.account_settings, menu);
131        super.onCreateOptionsMenu(menu, inflater);
132    }
133
134    @Override
135    public void onPrepareOptionsMenu(Menu menu) {
136        final UserHandle currentProfile = Process.myUserHandle();
137        if (mProfiles.size() == 1) {
138            menu.findItem(R.id.account_settings_menu_auto_sync)
139                    .setVisible(true)
140                    .setOnMenuItemClickListener(new MasterSyncStateClickListener(currentProfile))
141                    .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser(
142                            currentProfile.getIdentifier()));
143            menu.findItem(R.id.account_settings_menu_auto_sync_personal).setVisible(false);
144            menu.findItem(R.id.account_settings_menu_auto_sync_work).setVisible(false);
145        } else if (mProfiles.size() > 1) {
146            // We assume there's only one managed profile, otherwise UI needs to change
147            final UserHandle managedProfile = mProfiles.valueAt(1).userInfo.getUserHandle();
148
149            menu.findItem(R.id.account_settings_menu_auto_sync_personal)
150                    .setVisible(true)
151                    .setOnMenuItemClickListener(new MasterSyncStateClickListener(currentProfile))
152                    .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser(
153                            currentProfile.getIdentifier()));
154            menu.findItem(R.id.account_settings_menu_auto_sync_work)
155                    .setVisible(true)
156                    .setOnMenuItemClickListener(new MasterSyncStateClickListener(managedProfile))
157                    .setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser(
158                            managedProfile.getIdentifier()));
159            menu.findItem(R.id.account_settings_menu_auto_sync).setVisible(false);
160         } else {
161             Log.w(TAG, "Method onPrepareOptionsMenu called before mProfiles was initialized");
162         }
163    }
164
165    @Override
166    public void onResume() {
167        super.onResume();
168        updateUi();
169        mManagedProfileBroadcastReceiver.register(getActivity());
170        listenToAccountUpdates();
171    }
172
173    @Override
174    public void onPause() {
175        super.onPause();
176        stopListeningToAccountUpdates();
177        mManagedProfileBroadcastReceiver.unregister(getActivity());
178        cleanUpPreferences();
179    }
180
181    @Override
182    public void onAccountsUpdate(UserHandle userHandle) {
183        final ProfileData profileData = mProfiles.get(userHandle.getIdentifier());
184        if (profileData != null) {
185            updateAccountTypes(profileData);
186        } else {
187            Log.w(TAG, "Missing Settings screen for: " + userHandle.getIdentifier());
188        }
189    }
190
191    @Override
192    public boolean onPreferenceClick(Preference preference) {
193        // Check the preference
194        final int count = mProfiles.size();
195        for (int i = 0; i < count; i++) {
196            ProfileData profileData = mProfiles.valueAt(i);
197            if (preference == profileData.addAccountPreference) {
198                Intent intent = new Intent(ADD_ACCOUNT_ACTION);
199                intent.putExtra(EXTRA_USER, profileData.userInfo.getUserHandle());
200                intent.putExtra(EXTRA_AUTHORITIES, mAuthorities);
201                startActivity(intent);
202                return true;
203            }
204            if (preference == profileData.removeWorkProfilePreference) {
205                final int userId = profileData.userInfo.id;
206                Utils.createRemoveConfirmationDialog(getActivity(), userId,
207                        new DialogInterface.OnClickListener() {
208                            @Override
209                            public void onClick(DialogInterface dialog, int which) {
210                                mUm.removeUser(userId);
211                            }
212                        }
213                ).show();
214                return true;
215            }
216        }
217        return false;
218    }
219
220    void updateUi() {
221        // Load the preferences from an XML resource
222        addPreferencesFromResource(R.xml.account_settings);
223
224        if (Utils.isManagedProfile(mUm)) {
225            // This should not happen
226            Log.e(TAG, "We should not be showing settings for a managed profile");
227            finish();
228            return;
229        }
230
231        final PreferenceScreen preferenceScreen = (PreferenceScreen) findPreference(KEY_ACCOUNT);
232        if(mUm.isLinkedUser()) {
233            // Restricted user or similar
234            UserInfo userInfo = mUm.getUserInfo(UserHandle.myUserId());
235            updateProfileUi(userInfo, false /* no category needed */, preferenceScreen);
236        } else {
237            List<UserInfo> profiles = mUm.getProfiles(UserHandle.myUserId());
238            final int profilesCount = profiles.size();
239            final boolean addCategory = profilesCount > 1;
240            for (int i = 0; i < profilesCount; i++) {
241                updateProfileUi(profiles.get(i), addCategory, preferenceScreen);
242            }
243        }
244
245        // Add all preferences, starting with one for the primary profile.
246        // Note that we're relying on the ordering given by the SparseArray keys, and on the
247        // value of UserHandle.USER_OWNER being smaller than all the rest.
248        final int profilesCount = mProfiles.size();
249        for (int i = 0; i < profilesCount; i++) {
250            ProfileData profileData = mProfiles.valueAt(i);
251            if (!profileData.preferenceGroup.equals(preferenceScreen)) {
252                preferenceScreen.addPreference(profileData.preferenceGroup);
253            }
254            updateAccountTypes(profileData);
255        }
256    }
257
258    private void updateProfileUi(final UserInfo userInfo, boolean addCategory,
259            PreferenceScreen parent) {
260        final Context context = getActivity();
261        final ProfileData profileData = new ProfileData();
262        profileData.userInfo = userInfo;
263        if (addCategory) {
264            profileData.preferenceGroup = new PreferenceCategory(context);
265            profileData.preferenceGroup.setTitle(userInfo.isManagedProfile()
266                    ? R.string.category_work : R.string.category_personal);
267            parent.addPreference(profileData.preferenceGroup);
268        } else {
269            profileData.preferenceGroup = parent;
270        }
271        if (userInfo.isEnabled()) {
272            profileData.authenticatorHelper = new AuthenticatorHelper(context,
273                    userInfo.getUserHandle(), mUm, this);
274            if (!mUm.hasUserRestriction(DISALLOW_MODIFY_ACCOUNTS, userInfo.getUserHandle())) {
275                profileData.addAccountPreference = newAddAccountPreference(context);
276            }
277        }
278        if (userInfo.isManagedProfile()) {
279            profileData.removeWorkProfilePreference = newRemoveWorkProfilePreference(context);
280        }
281        mProfiles.put(userInfo.id, profileData);
282    }
283
284    private Preference newAddAccountPreference(Context context) {
285        Preference preference = new Preference(context);
286        preference.setTitle(R.string.add_account_label);
287        preference.setIcon(R.drawable.ic_menu_add_dark);
288        preference.setOnPreferenceClickListener(this);
289        preference.setOrder(ORDER_NEXT_TO_LAST);
290        return preference;
291    }
292
293    private Preference newRemoveWorkProfilePreference(Context context) {
294        Preference preference = new Preference(context);
295        preference.setTitle(R.string.remove_managed_profile_label);
296        preference.setIcon(R.drawable.ic_menu_delete);
297        preference.setOnPreferenceClickListener(this);
298        preference.setOrder(ORDER_LAST);
299        return preference;
300    }
301
302    private void cleanUpPreferences() {
303        PreferenceScreen preferenceScreen = getPreferenceScreen();
304        if (preferenceScreen != null) {
305            preferenceScreen.removeAll();
306        }
307        mProfiles.clear();
308    }
309
310    private void listenToAccountUpdates() {
311        final int count = mProfiles.size();
312        for (int i = 0; i < count; i++) {
313            AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper;
314            if (authenticatorHelper != null) {
315                authenticatorHelper.listenToAccountUpdates();
316            }
317        }
318    }
319
320    private void stopListeningToAccountUpdates() {
321        final int count = mProfiles.size();
322        for (int i = 0; i < count; i++) {
323            AuthenticatorHelper authenticatorHelper = mProfiles.valueAt(i).authenticatorHelper;
324            if (authenticatorHelper != null) {
325                authenticatorHelper.stopListeningToAccountUpdates();
326            }
327        }
328    }
329
330    private void updateAccountTypes(ProfileData profileData) {
331        profileData.preferenceGroup.removeAll();
332        if (profileData.userInfo.isEnabled()) {
333            final ArrayList<AccountPreference> preferences = getAccountTypePreferences(
334                    profileData.authenticatorHelper, profileData.userInfo.getUserHandle());
335            final int count = preferences.size();
336            for (int i = 0; i < count; i++) {
337                profileData.preferenceGroup.addPreference(preferences.get(i));
338            }
339            if (profileData.addAccountPreference != null) {
340                profileData.preferenceGroup.addPreference(profileData.addAccountPreference);
341            }
342        } else {
343            // Put a label instead of the accounts list
344            mProfileNotAvailablePreference.setEnabled(false);
345            mProfileNotAvailablePreference.setIcon(R.drawable.empty_icon);
346            mProfileNotAvailablePreference.setTitle(null);
347            mProfileNotAvailablePreference.setSummary(
348                    R.string.managed_profile_not_available_label);
349            profileData.preferenceGroup.addPreference(mProfileNotAvailablePreference);
350        }
351        if (profileData.removeWorkProfilePreference != null) {
352            profileData.preferenceGroup.addPreference(profileData.removeWorkProfilePreference);
353        }
354    }
355
356    private ArrayList<AccountPreference> getAccountTypePreferences(AuthenticatorHelper helper,
357            UserHandle userHandle) {
358        final String[] accountTypes = helper.getEnabledAccountTypes();
359        final ArrayList<AccountPreference> accountTypePreferences =
360                new ArrayList<AccountPreference>(accountTypes.length);
361
362        for (int i = 0; i < accountTypes.length; i++) {
363            final String accountType = accountTypes[i];
364            // Skip showing any account that does not have any of the requested authorities
365            if (!accountTypeHasAnyRequestedAuthorities(helper, accountType)) {
366                continue;
367            }
368            final CharSequence label = helper.getLabelForType(getActivity(), accountType);
369            if (label == null) {
370                continue;
371            }
372
373            final Account[] accounts = AccountManager.get(getActivity())
374                    .getAccountsByTypeAsUser(accountType, userHandle);
375            final boolean skipToAccount = accounts.length == 1
376                    && !helper.hasAccountPreferences(accountType);
377
378            if (skipToAccount) {
379                final Bundle fragmentArguments = new Bundle();
380                fragmentArguments.putParcelable(AccountSyncSettings.ACCOUNT_KEY,
381                        accounts[0]);
382                fragmentArguments.putParcelable(EXTRA_USER, userHandle);
383
384                accountTypePreferences.add(new AccountPreference(getActivity(), label,
385                        AccountSyncSettings.class.getName(), fragmentArguments,
386                        helper.getDrawableForType(getActivity(), accountType)));
387            } else {
388                final Bundle fragmentArguments = new Bundle();
389                fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_TYPE, accountType);
390                fragmentArguments.putString(ManageAccountsSettings.KEY_ACCOUNT_LABEL,
391                        label.toString());
392                fragmentArguments.putParcelable(EXTRA_USER, userHandle);
393
394                accountTypePreferences.add(new AccountPreference(getActivity(), label,
395                        ManageAccountsSettings.class.getName(), fragmentArguments,
396                        helper.getDrawableForType(getActivity(), accountType)));
397            }
398            helper.preloadDrawableForType(getActivity(), accountType);
399        }
400        // Sort by label
401        Collections.sort(accountTypePreferences, new Comparator<AccountPreference>() {
402            @Override
403            public int compare(AccountPreference t1, AccountPreference t2) {
404                return t1.mTitle.toString().compareTo(t2.mTitle.toString());
405            }
406        });
407        return accountTypePreferences;
408    }
409
410    private boolean accountTypeHasAnyRequestedAuthorities(AuthenticatorHelper helper,
411            String accountType) {
412        if (mAuthoritiesCount == 0) {
413            // No authorities required
414            return true;
415        }
416        final ArrayList<String> authoritiesForType = helper.getAuthoritiesForAccountType(
417                accountType);
418        if (authoritiesForType == null) {
419            Log.d(TAG, "No sync authorities for account type: " + accountType);
420            return false;
421        }
422        for (int j = 0; j < mAuthoritiesCount; j++) {
423            if (authoritiesForType.contains(mAuthorities[j])) {
424                return true;
425            }
426        }
427        return false;
428    }
429
430    private class AccountPreference extends Preference implements OnPreferenceClickListener {
431        /**
432         * Title of the tile that is shown to the user.
433         * @attr ref android.R.styleable#PreferenceHeader_title
434         */
435        private final CharSequence mTitle;
436
437        /**
438         * Full class name of the fragment to display when this tile is
439         * selected.
440         * @attr ref android.R.styleable#PreferenceHeader_fragment
441         */
442        private final String mFragment;
443
444        /**
445         * Optional arguments to supply to the fragment when it is
446         * instantiated.
447         */
448        private final Bundle mFragmentArguments;
449
450        public AccountPreference(Context context, CharSequence title, String fragment,
451                Bundle fragmentArguments, Drawable icon) {
452            super(context);
453            mTitle = title;
454            mFragment = fragment;
455            mFragmentArguments = fragmentArguments;
456            setWidgetLayoutResource(R.layout.account_type_preference);
457
458            setTitle(title);
459            setIcon(icon);
460
461            setOnPreferenceClickListener(this);
462        }
463
464        @Override
465        public boolean onPreferenceClick(Preference preference) {
466            if (mFragment != null) {
467                Utils.startWithFragment(
468                        getContext(), mFragment, mFragmentArguments, null, 0, 0, mTitle);
469                return true;
470            }
471            return false;
472        }
473    }
474
475    private class ManagedProfileBroadcastReceiver extends BroadcastReceiver {
476        private boolean listeningToManagedProfileEvents;
477
478        @Override
479        public void onReceive(Context context, Intent intent) {
480            if (intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_REMOVED)
481                    || intent.getAction().equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) {
482                Log.v(TAG, "Received broadcast: " + intent.getAction());
483                // Clean old state
484                stopListeningToAccountUpdates();
485                cleanUpPreferences();
486                // Build new state
487                updateUi();
488                listenToAccountUpdates();
489                // Force the menu to update. Note that #onPrepareOptionsMenu uses data built by
490                // #updateUi so we must call this later
491                getActivity().invalidateOptionsMenu();
492                return;
493            }
494            Log.w(TAG, "Cannot handle received broadcast: " + intent.getAction());
495        }
496
497        public void register(Context context) {
498            if (!listeningToManagedProfileEvents) {
499                IntentFilter intentFilter = new IntentFilter();
500                intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
501                intentFilter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
502                context.registerReceiver(this, intentFilter);
503                listeningToManagedProfileEvents = true;
504            }
505        }
506
507        public void unregister(Context context) {
508            if (listeningToManagedProfileEvents) {
509                context.unregisterReceiver(this);
510                listeningToManagedProfileEvents = false;
511            }
512        }
513    }
514
515    private class MasterSyncStateClickListener implements MenuItem.OnMenuItemClickListener {
516        private final UserHandle mUserHandle;
517
518        public MasterSyncStateClickListener(UserHandle userHandle) {
519            mUserHandle = userHandle;
520        }
521
522        @Override
523        public boolean onMenuItemClick(MenuItem item) {
524            if (ActivityManager.isUserAMonkey()) {
525                Log.d(TAG, "ignoring monkey's attempt to flip sync state");
526            } else {
527                ConfirmAutoSyncChangeFragment.show(AccountSettings.this, !item.isChecked(),
528                        mUserHandle);
529            }
530            return true;
531        }
532    }
533
534    /**
535     * Dialog to inform user about changing auto-sync setting
536     */
537    public static class ConfirmAutoSyncChangeFragment extends DialogFragment {
538        private static final String SAVE_ENABLING = "enabling";
539        private boolean mEnabling;
540        private UserHandle mUserHandle;
541
542        public static void show(AccountSettings parent, boolean enabling, UserHandle userHandle) {
543            if (!parent.isAdded()) return;
544
545            final ConfirmAutoSyncChangeFragment dialog = new ConfirmAutoSyncChangeFragment();
546            dialog.mEnabling = enabling;
547            dialog.mUserHandle = userHandle;
548            dialog.setTargetFragment(parent, 0);
549            dialog.show(parent.getFragmentManager(), TAG_CONFIRM_AUTO_SYNC_CHANGE);
550        }
551
552        @Override
553        public Dialog onCreateDialog(Bundle savedInstanceState) {
554            final Context context = getActivity();
555            if (savedInstanceState != null) {
556                mEnabling = savedInstanceState.getBoolean(SAVE_ENABLING);
557            }
558
559            final AlertDialog.Builder builder = new AlertDialog.Builder(context);
560            if (!mEnabling) {
561                builder.setTitle(R.string.data_usage_auto_sync_off_dialog_title);
562                builder.setMessage(R.string.data_usage_auto_sync_off_dialog);
563            } else {
564                builder.setTitle(R.string.data_usage_auto_sync_on_dialog_title);
565                builder.setMessage(R.string.data_usage_auto_sync_on_dialog);
566            }
567
568            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
569                @Override
570                public void onClick(DialogInterface dialog, int which) {
571                    ContentResolver.setMasterSyncAutomaticallyAsUser(mEnabling,
572                            mUserHandle.getIdentifier());
573                }
574            });
575            builder.setNegativeButton(android.R.string.cancel, null);
576
577            return builder.create();
578        }
579
580        @Override
581        public void onSaveInstanceState(Bundle outState) {
582            super.onSaveInstanceState(outState);
583            outState.putBoolean(SAVE_ENABLING, mEnabling);
584        }
585    }
586    // TODO Implement a {@link SearchIndexProvider} to allow Indexing and Search of account types
587    // See http://b/15403806
588}
589