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
onCreate(final Bundle icicle)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 
ManagedProfileCheckerTask(final AccountsSettingsFragment fragment)152         private ManagedProfileCheckerTask(final AccountsSettingsFragment fragment) {
153             mFragment = fragment;
154         }
155 
156         @Override
onPreExecute()157         protected void onPreExecute() {
158             mFragment.mManagedProfileBeingDetected.set(true);
159         }
160         @Override
doInBackground(Void... params)161         protected Boolean doInBackground(Void... params) {
162             return ManagedProfileUtils.getInstance().hasWorkProfile(mFragment.getActivity());
163         }
164 
165         @Override
onPostExecute(final Boolean hasWorkProfile)166         protected void onPostExecute(final Boolean hasWorkProfile) {
167             mFragment.mHasManagedProfile.set(hasWorkProfile);
168             mFragment.mManagedProfileBeingDetected.set(false);
169             mFragment.refreshSyncSettingsUI();
170         }
171     }
172 
enableSyncPreferences(final String[] accountsForLogin, final String currentAccountName)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      */
disableSyncPreferences()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      */
removeSyncPreferences()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
onResume()229     public void onResume() {
230         super.onResume();
231         refreshSyncSettingsUI();
232     }
233 
234     @Override
onSharedPreferenceChanged(final SharedPreferences prefs, final String key)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      */
refreshSyncSettingsUI()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      */
refreshSyncSettingsMessaging(boolean hasAccountsPermission, boolean managedProfileBeingDetected, boolean hasManagedProfile, boolean hasAccountsForLogin, String currentAccount)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
getSignedInAccountName()329     String getSignedInAccountName() {
330         return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
331     }
332 
isSyncEnabled()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
createAccountPicker(final String[] accounts, final String selectedAccount, final DialogInterface.OnClickListener positiveButtonClickListener)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 
AccountChangedListener(final TwoStatePreference dependentPreference)390         AccountChangedListener(final TwoStatePreference dependentPreference) {
391             mDependentPreference = dependentPreference;
392         }
393 
394         @Override
onClick(final DialogInterface dialog, final int which)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
onPreferenceClick(final Preference preference)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
onPreferenceClick(final Preference preference)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
onPreferenceClick(final Preference preference)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
onShow(DialogInterface dialog)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