Home | History | Annotate | Download | only in settings
      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 
     17 package com.android.inputmethod.latin.settings;
     18 
     19 import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME;
     20 import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC;
     21 
     22 import android.Manifest;
     23 import android.app.AlertDialog;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.content.DialogInterface.OnShowListener;
     27 import android.content.SharedPreferences;
     28 import android.content.res.Resources;
     29 import android.os.AsyncTask;
     30 import android.os.Bundle;
     31 import android.preference.Preference;
     32 import android.preference.Preference.OnPreferenceClickListener;
     33 import android.preference.TwoStatePreference;
     34 import android.text.TextUtils;
     35 import android.text.method.LinkMovementMethod;
     36 import android.widget.ListView;
     37 import android.widget.TextView;
     38 
     39 import com.android.inputmethod.annotations.UsedForTesting;
     40 import com.android.inputmethod.latin.R;
     41 import com.android.inputmethod.latin.accounts.AccountStateChangedListener;
     42 import com.android.inputmethod.latin.accounts.LoginAccountUtils;
     43 import com.android.inputmethod.latin.define.ProductionFlags;
     44 import com.android.inputmethod.latin.permissions.PermissionsUtil;
     45 import com.android.inputmethod.latin.utils.ManagedProfileUtils;
     46 
     47 import java.util.concurrent.atomic.AtomicBoolean;
     48 
     49 import javax.annotation.Nullable;
     50 
     51 /**
     52  * "Accounts & Privacy" settings sub screen.
     53  *
     54  * This settings sub screen handles the following preferences:
     55  * <li> Account selection/management for IME </li>
     56  * <li> Sync preferences </li>
     57  * <li> Privacy preferences </li>
     58  */
     59 public final class AccountsSettingsFragment extends SubScreenFragment {
     60     private static final String PREF_ENABLE_SYNC_NOW = "pref_enable_cloud_sync";
     61     private static final String PREF_SYNC_NOW = "pref_sync_now";
     62     private static final String PREF_CLEAR_SYNC_DATA = "pref_clear_sync_data";
     63 
     64     static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
     65 
     66     /**
     67      * Onclick listener for sync now pref.
     68      */
     69     private final Preference.OnPreferenceClickListener mSyncNowListener =
     70             new SyncNowListener();
     71     /**
     72      * Onclick listener for delete sync pref.
     73      */
     74     private final Preference.OnPreferenceClickListener mDeleteSyncDataListener =
     75             new DeleteSyncDataListener();
     76 
     77     /**
     78      * Onclick listener for enable sync pref.
     79      */
     80     private final Preference.OnPreferenceClickListener mEnableSyncClickListener =
     81             new EnableSyncClickListener();
     82 
     83     /**
     84      * Enable sync checkbox pref.
     85      */
     86     private TwoStatePreference mEnableSyncPreference;
     87 
     88     /**
     89      * Enable sync checkbox pref.
     90      */
     91     private Preference mSyncNowPreference;
     92 
     93     /**
     94      * Clear sync data pref.
     95      */
     96     private Preference mClearSyncDataPreference;
     97 
     98     /**
     99      * Account switcher preference.
    100      */
    101     private Preference mAccountSwitcher;
    102 
    103     /**
    104      * Stores if we are currently detecting a managed profile.
    105      */
    106     private AtomicBoolean mManagedProfileBeingDetected = new AtomicBoolean(true);
    107 
    108     /**
    109      * Stores if we have successfully detected if the device has a managed profile.
    110      */
    111     private AtomicBoolean mHasManagedProfile = new AtomicBoolean(false);
    112 
    113     @Override
    114     public void onCreate(final Bundle icicle) {
    115         super.onCreate(icicle);
    116         addPreferencesFromResource(R.xml.prefs_screen_accounts);
    117 
    118         mAccountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
    119         mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW);
    120         mSyncNowPreference = findPreference(PREF_SYNC_NOW);
    121         mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA);
    122 
    123         if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
    124             final Preference enableMetricsLogging =
    125                     findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
    126             final Resources res = getResources();
    127             if (enableMetricsLogging != null) {
    128                 final String enableMetricsLoggingTitle = res.getString(
    129                         R.string.enable_metrics_logging, getApplicationName());
    130                 enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
    131             }
    132         } else {
    133             removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
    134         }
    135 
    136         if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
    137             removeSyncPreferences();
    138         } else {
    139             // Disable by default till we are sure we can enable this.
    140             disableSyncPreferences();
    141             new ManagedProfileCheckerTask(this).execute();
    142         }
    143     }
    144 
    145     /**
    146      * Task to check work profile. If found, it removes the sync prefs. If not,
    147      * it enables them.
    148      */
    149     private static class ManagedProfileCheckerTask extends AsyncTask<Void, Void, Boolean> {
    150         private final AccountsSettingsFragment mFragment;
    151 
    152         private ManagedProfileCheckerTask(final AccountsSettingsFragment fragment) {
    153             mFragment = fragment;
    154         }
    155 
    156         @Override
    157         protected void onPreExecute() {
    158             mFragment.mManagedProfileBeingDetected.set(true);
    159         }
    160         @Override
    161         protected Boolean doInBackground(Void... params) {
    162             return ManagedProfileUtils.getInstance().hasWorkProfile(mFragment.getActivity());
    163         }
    164 
    165         @Override
    166         protected void onPostExecute(final Boolean hasWorkProfile) {
    167             mFragment.mHasManagedProfile.set(hasWorkProfile);
    168             mFragment.mManagedProfileBeingDetected.set(false);
    169             mFragment.refreshSyncSettingsUI();
    170         }
    171     }
    172 
    173     private void enableSyncPreferences(final String[] accountsForLogin,
    174             final String currentAccountName) {
    175         if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
    176             return;
    177         }
    178         mAccountSwitcher.setEnabled(true);
    179 
    180         mEnableSyncPreference.setEnabled(true);
    181         mEnableSyncPreference.setOnPreferenceClickListener(mEnableSyncClickListener);
    182 
    183         mSyncNowPreference.setEnabled(true);
    184         mSyncNowPreference.setOnPreferenceClickListener(mSyncNowListener);
    185 
    186         mClearSyncDataPreference.setEnabled(true);
    187         mClearSyncDataPreference.setOnPreferenceClickListener(mDeleteSyncDataListener);
    188 
    189         if (currentAccountName != null) {
    190             mAccountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
    191                 @Override
    192                 public boolean onPreferenceClick(final Preference preference) {
    193                     if (accountsForLogin.length > 0) {
    194                         // TODO: Add addition of account.
    195                         createAccountPicker(accountsForLogin, getSignedInAccountName(),
    196                                 new AccountChangedListener(null)).show();
    197                     }
    198                     return true;
    199                 }
    200             });
    201         }
    202     }
    203 
    204     /**
    205      * Two reasons for disable - work profile or no accounts on device.
    206      */
    207     private void disableSyncPreferences() {
    208         if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
    209             return;
    210         }
    211 
    212         mAccountSwitcher.setEnabled(false);
    213         mEnableSyncPreference.setEnabled(false);
    214         mSyncNowPreference.setEnabled(false);
    215         mClearSyncDataPreference.setEnabled(false);
    216     }
    217 
    218     /**
    219      * Called only when ProductionFlag is turned off.
    220      */
    221     private void removeSyncPreferences() {
    222         removePreference(PREF_ACCCOUNT_SWITCHER);
    223         removePreference(PREF_ENABLE_CLOUD_SYNC);
    224         removePreference(PREF_SYNC_NOW);
    225         removePreference(PREF_CLEAR_SYNC_DATA);
    226     }
    227 
    228     @Override
    229     public void onResume() {
    230         super.onResume();
    231         refreshSyncSettingsUI();
    232     }
    233 
    234     @Override
    235     public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
    236         if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) {
    237             refreshSyncSettingsUI();
    238         } else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) {
    239             mEnableSyncPreference = (TwoStatePreference) findPreference(PREF_ENABLE_SYNC_NOW);
    240             final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
    241             if (isSyncEnabled()) {
    242                 mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary));
    243             } else {
    244                 mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
    245             }
    246             AccountStateChangedListener.onSyncPreferenceChanged(getSignedInAccountName(),
    247                     syncEnabled);
    248         }
    249     }
    250 
    251     /**
    252      * Checks different states like whether account is present or managed profile is present
    253      * and sets the sync settings accordingly.
    254      */
    255     private void refreshSyncSettingsUI() {
    256         if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
    257             return;
    258         }
    259         boolean hasAccountsPermission = PermissionsUtil.checkAllPermissionsGranted(
    260             getActivity(), Manifest.permission.READ_CONTACTS);
    261 
    262         final String[] accountsForLogin = hasAccountsPermission ?
    263                 LoginAccountUtils.getAccountsForLogin(getActivity()) : new String[0];
    264         final String currentAccount = hasAccountsPermission ? getSignedInAccountName() : null;
    265 
    266         if (hasAccountsPermission && !mManagedProfileBeingDetected.get() &&
    267                 !mHasManagedProfile.get() && accountsForLogin.length > 0) {
    268             // Sync can be used by user; enable all preferences.
    269             enableSyncPreferences(accountsForLogin, currentAccount);
    270         } else {
    271             // Sync cannot be used by user; disable all preferences.
    272             disableSyncPreferences();
    273         }
    274         refreshSyncSettingsMessaging(hasAccountsPermission, mManagedProfileBeingDetected.get(),
    275                 mHasManagedProfile.get(), accountsForLogin.length > 0,
    276                 currentAccount);
    277     }
    278 
    279     /**
    280      * @param hasAccountsPermission whether the app has the permission to read accounts.
    281      * @param managedProfileBeingDetected whether we are in process of determining work profile.
    282      * @param hasManagedProfile whether the device has work profile.
    283      * @param hasAccountsForLogin whether the device has enough accounts for login.
    284      * @param currentAccount the account currently selected in the application.
    285      */
    286     private void refreshSyncSettingsMessaging(boolean hasAccountsPermission,
    287                                               boolean managedProfileBeingDetected,
    288                                               boolean hasManagedProfile,
    289                                               boolean hasAccountsForLogin,
    290                                               String currentAccount) {
    291         if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
    292             return;
    293         }
    294 
    295         if (!hasAccountsPermission) {
    296             mEnableSyncPreference.setChecked(false);
    297             mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
    298             mAccountSwitcher.setSummary("");
    299             return;
    300         } else if (managedProfileBeingDetected) {
    301             // If we are determining eligiblity, we show empty summaries.
    302             // Once we have some deterministic result, we set summaries based on different results.
    303             mEnableSyncPreference.setSummary("");
    304             mAccountSwitcher.setSummary("");
    305         } else if (hasManagedProfile) {
    306             mEnableSyncPreference.setSummary(
    307                     getString(R.string.cloud_sync_summary_disabled_work_profile));
    308         } else if (!hasAccountsForLogin) {
    309             mEnableSyncPreference.setSummary(getString(R.string.add_account_to_enable_sync));
    310         } else if (isSyncEnabled()) {
    311             mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary));
    312         } else {
    313             mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
    314         }
    315 
    316         // Set some interdependent settings.
    317         // No account automatically turns off sync.
    318         if (!managedProfileBeingDetected && !hasManagedProfile) {
    319             if (currentAccount != null) {
    320                 mAccountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
    321             } else {
    322                 mEnableSyncPreference.setChecked(false);
    323                 mAccountSwitcher.setSummary(getString(R.string.no_accounts_selected));
    324             }
    325         }
    326     }
    327 
    328     @Nullable
    329     String getSignedInAccountName() {
    330         return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
    331     }
    332 
    333     boolean isSyncEnabled() {
    334         return getSharedPreferences().getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
    335     }
    336 
    337     /**
    338      * Creates an account picker dialog showing the given accounts in a list and selecting
    339      * the selected account by default.  The list of accounts must not be null/empty.
    340      *
    341      * Package-private for testing.
    342      *
    343      * @param accounts list of accounts on the device.
    344      * @param selectedAccount currently selected account
    345      * @param positiveButtonClickListener listener that gets called when positive button is
    346      * clicked
    347      */
    348     @UsedForTesting
    349     AlertDialog createAccountPicker(final String[] accounts,
    350             final String selectedAccount,
    351             final DialogInterface.OnClickListener positiveButtonClickListener) {
    352         if (accounts == null || accounts.length == 0) {
    353             throw new IllegalArgumentException("List of accounts must not be empty");
    354         }
    355 
    356         // See if the currently selected account is in the list.
    357         // If it is, the entry is selected, and a sign-out button is provided.
    358         // If it isn't, select the 0th account by default which will get picked up
    359         // if the user presses OK.
    360         int index = 0;
    361         boolean isSignedIn = false;
    362         for (int i = 0;  i < accounts.length; i++) {
    363             if (TextUtils.equals(accounts[i], selectedAccount)) {
    364                 index = i;
    365                 isSignedIn = true;
    366                 break;
    367             }
    368         }
    369         final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
    370                 .setTitle(R.string.account_select_title)
    371                 .setSingleChoiceItems(accounts, index, null)
    372                 .setPositiveButton(R.string.account_select_ok, positiveButtonClickListener)
    373                 .setNegativeButton(R.string.account_select_cancel, null);
    374         if (isSignedIn) {
    375             builder.setNeutralButton(R.string.account_select_sign_out, positiveButtonClickListener);
    376         }
    377         return builder.create();
    378     }
    379 
    380     /**
    381      * Listener for a account selection changes from the picker.
    382      * Persists/removes the account to/from shared preferences and sets up sync if required.
    383      */
    384     class AccountChangedListener implements DialogInterface.OnClickListener {
    385         /**
    386          * Represents preference that should be changed based on account chosen.
    387          */
    388         private TwoStatePreference mDependentPreference;
    389 
    390         AccountChangedListener(final TwoStatePreference dependentPreference) {
    391             mDependentPreference = dependentPreference;
    392         }
    393 
    394         @Override
    395         public void onClick(final DialogInterface dialog, final int which) {
    396             final String oldAccount = getSignedInAccountName();
    397             switch (which) {
    398                 case DialogInterface.BUTTON_POSITIVE: // Signed in
    399                     final ListView lv = ((AlertDialog)dialog).getListView();
    400                     final String newAccount =
    401                             (String) lv.getItemAtPosition(lv.getCheckedItemPosition());
    402                     getSharedPreferences()
    403                             .edit()
    404                             .putString(PREF_ACCOUNT_NAME, newAccount)
    405                             .apply();
    406                     AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount);
    407                     if (mDependentPreference != null) {
    408                         mDependentPreference.setChecked(true);
    409                     }
    410                     break;
    411                 case DialogInterface.BUTTON_NEUTRAL: // Signed out
    412                     AccountStateChangedListener.onAccountSignedOut(oldAccount);
    413                     getSharedPreferences()
    414                             .edit()
    415                             .remove(PREF_ACCOUNT_NAME)
    416                             .apply();
    417                     break;
    418             }
    419         }
    420     }
    421 
    422     /**
    423      * Listener that initiates the process of sync in the background.
    424      */
    425     class SyncNowListener implements Preference.OnPreferenceClickListener {
    426         @Override
    427         public boolean onPreferenceClick(final Preference preference) {
    428             AccountStateChangedListener.forceSync(getSignedInAccountName());
    429             return true;
    430         }
    431     }
    432 
    433     /**
    434      * Listener that initiates the process of deleting user's data from the cloud.
    435      */
    436     class DeleteSyncDataListener implements Preference.OnPreferenceClickListener {
    437         @Override
    438         public boolean onPreferenceClick(final Preference preference) {
    439             final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity())
    440                     .setTitle(R.string.clear_sync_data_title)
    441                     .setMessage(R.string.clear_sync_data_confirmation)
    442                     .setPositiveButton(R.string.clear_sync_data_ok,
    443                             new DialogInterface.OnClickListener() {
    444                                 @Override
    445                                 public void onClick(final DialogInterface dialog, final int which) {
    446                                     if (which == DialogInterface.BUTTON_POSITIVE) {
    447                                         AccountStateChangedListener.forceDelete(
    448                                                 getSignedInAccountName());
    449                                     }
    450                                 }
    451                              })
    452                     .setNegativeButton(R.string.cloud_sync_cancel, null /* OnClickListener */)
    453                     .create();
    454             confirmationDialog.show();
    455             return true;
    456         }
    457     }
    458 
    459     /**
    460      * Listens to events when user clicks on "Enable sync" feature.
    461      */
    462     class EnableSyncClickListener implements OnShowListener, Preference.OnPreferenceClickListener {
    463         // TODO(cvnguyen): Write tests.
    464         @Override
    465         public boolean onPreferenceClick(final Preference preference) {
    466             final TwoStatePreference syncPreference = (TwoStatePreference) preference;
    467             if (syncPreference.isChecked()) {
    468                 // Uncheck for now.
    469                 syncPreference.setChecked(false);
    470 
    471                 // Show opt-in.
    472                 final AlertDialog optInDialog = new AlertDialog.Builder(getActivity())
    473                         .setTitle(R.string.cloud_sync_title)
    474                         .setMessage(R.string.cloud_sync_opt_in_text)
    475                         .setPositiveButton(R.string.account_select_ok,
    476                                 new DialogInterface.OnClickListener() {
    477                                     @Override
    478                                     public void onClick(final DialogInterface dialog,
    479                                                         final int which) {
    480                                         if (which == DialogInterface.BUTTON_POSITIVE) {
    481                                             final Context context = getActivity();
    482                                             final String[] accountsForLogin =
    483                                                     LoginAccountUtils.getAccountsForLogin(context);
    484                                             createAccountPicker(accountsForLogin,
    485                                                     getSignedInAccountName(),
    486                                                     new AccountChangedListener(syncPreference))
    487                                                     .show();
    488                                         }
    489                                     }
    490                         })
    491                         .setNegativeButton(R.string.cloud_sync_cancel, null)
    492                         .create();
    493                 optInDialog.setOnShowListener(this);
    494                 optInDialog.show();
    495             }
    496             return true;
    497         }
    498 
    499         @Override
    500         public void onShow(DialogInterface dialog) {
    501             TextView messageView = (TextView) ((AlertDialog) dialog).findViewById(
    502                     android.R.id.message);
    503             if (messageView != null) {
    504                 messageView.setMovementMethod(LinkMovementMethod.getInstance());
    505             }
    506         }
    507     }
    508 }
    509