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