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.inputmethod.latin.settings;
18
19import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME;
20import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC;
21
22import android.Manifest;
23import android.app.AlertDialog;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.content.DialogInterface.OnShowListener;
27import android.content.SharedPreferences;
28import android.content.res.Resources;
29import android.os.AsyncTask;
30import android.os.Bundle;
31import android.preference.Preference;
32import android.preference.Preference.OnPreferenceClickListener;
33import android.preference.TwoStatePreference;
34import android.text.TextUtils;
35import android.text.method.LinkMovementMethod;
36import android.widget.ListView;
37import android.widget.TextView;
38
39import com.android.inputmethod.annotations.UsedForTesting;
40import com.android.inputmethod.latin.R;
41import com.android.inputmethod.latin.accounts.AccountStateChangedListener;
42import com.android.inputmethod.latin.accounts.LoginAccountUtils;
43import com.android.inputmethod.latin.define.ProductionFlags;
44import com.android.inputmethod.latin.permissions.PermissionsUtil;
45import com.android.inputmethod.latin.utils.ManagedProfileUtils;
46
47import java.util.concurrent.atomic.AtomicBoolean;
48
49import 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 */
59public 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