Refresh pref settings for cloud sync

- Show the opt-in text
- Show the account picker if user presses 'enable sync'. Previously
  we disabled the sync pref which was confusing.
- Remove the debug tags from some prefs; and improve messaging overall

General rules;
- sync is turned ON : user checks 'enable sync' pref AND
  accepts the opt-in AND chooses an account
- sync is turned OFF: when user signs out

Demo link
https://drive.google.com/a/google.com/file/d/0B9tNQOWdRuiWSUdVVE5rVDJudlk/view?usp=sharing

Change-Id: I2e7933796b15e47005ba9970a8c1294416ef31a0
main
Jatin Matani 2015-02-24 09:59:02 -08:00
parent 1d5ec6136c
commit 8ec328fb2e
4 changed files with 169 additions and 109 deletions

View File

@ -39,11 +39,9 @@
<!-- Settings screen title for preferences [CHAR LIMIT=33]--> <!-- Settings screen title for preferences [CHAR LIMIT=33]-->
<string name="settings_screen_preferences">Preferences</string> <string name="settings_screen_preferences">Preferences</string>
<!-- Settings screen title for accounts and privacy preferences [CHAR LIMIT=33]--> <!-- Settings screen title for accounts and privacy preferences [CHAR LIMIT=33]-->
<string name="settings_screen_accounts">Accounts &amp; privacy</string> <string name="settings_screen_accounts">Accounts &amp; Privacy</string>
<!-- Settings screen title for appearance & layouts preferences [CHAR LIMIT=33] --> <!-- Settings screen title for appearance & layouts preferences [CHAR LIMIT=33] -->
<string name="settings_screen_appearance">Appearance &amp; layouts</string> <string name="settings_screen_appearance">Appearance &amp; Layouts</string>
<!-- Settings screen title for multilingual options [CHAR_LIMIT=33] -->
<string name="settings_screen_multilingual">Multilingual options</string>
<!-- Settings screen title for gesture typing preferences [CHAR_LIMIT=33] --> <!-- Settings screen title for gesture typing preferences [CHAR_LIMIT=33] -->
<string name="settings_screen_gesture">Gesture Typing</string> <string name="settings_screen_gesture">Gesture Typing</string>
<!-- Settings screen title for text correction options [CHAR_LIMIT=33] --> <!-- Settings screen title for text correction options [CHAR_LIMIT=33] -->
@ -56,17 +54,31 @@
<!-- Option for enabling or disabling the split keyboard layout. [CHAR LIMIT=65]--> <!-- Option for enabling or disabling the split keyboard layout. [CHAR LIMIT=65]-->
<string name="enable_split_keyboard">Enable split keyboard</string> <string name="enable_split_keyboard">Enable split keyboard</string>
<!-- TODO: Enable translation for user-visible strings --> <!-- Option title for enabling cloud sync feature [CHAR LIMIT=33]-->
<string name="cloud_sync_title" translatable="false">Enable sync</string> <string name="cloud_sync_title">Google Keyboard Sync</string>
<string name="cloud_sync_summary" translatable="false">Sync your personal dictionary across devices</string> <!-- Option summary when cloud sync feature is enabled [CHAR LIMIT=65] -->
<string name="cloud_sync_summary_disabled_signed_out" translatable="false">Select an account to enable sync</string> <string name="cloud_sync_summary">Sync is turned on</string>
<string name="sync_now_title" translatable="false">[DEBUG] Sync Now</string> <!-- Option summary when cloud sync feature is disabled [CHAR LIMIT=65] -->
<string name="clear_sync_data_title" translatable="false">[DEBUG] Delete Google Keyboard cloud data</string> <string name="cloud_sync_summary_disabled">Sync your personal dictionary across devices</string>
<string name="clear_sync_data_summary" translable="false">Deletes your synced data from Google</string> <!-- Option title for starting the sync cycle now. [CHAR LIMIT=33]-->
<string name="clear_sync_data_confirmation" translable="false">Your synced data will be deleted. Are you sure?</string> <string name="sync_now_title">Sync Now</string>
<string name="clear_sync_data_ok" translable="false">Delete</string> <!-- Option title for letting user delete data from Google servers. [CHAR LIMIT=33] -->
<string name="clear_sync_data_cancel" translable="false">Cancel</string> <string name="clear_sync_data_title">Delete Keyboard Cloud data</string>
<!-- Option summary for letting user delete data from Google servers. [CHAR LIMIT=65] -->
<string name="clear_sync_data_summary">Deletes your synced data from Google</string>
<!-- Text for confirmation dialog box asking user to confirm deletion of cloud data. [CHAR LIMIT=65] -->
<string name="clear_sync_data_confirmation">Your synced data will be deleted from the cloud. Are you sure?</string>
<!-- Option to confirm deleting of user data from cloud [CHAR LIMIT=20] -->
<string name="clear_sync_data_ok">Delete</string>
<!-- Option to cancel the deletion of user data from cloud [CHAR LIMIT=20] -->
<string name="cloud_sync_cancel">Cancel</string>
<!-- Option to agree to terms and conditions for enabling cloud sync feature. -->
<string name="cloud_sync_opt_in_text">Your personal dictionary will be synced &amp; backed up to
Google servers. The statistical information of word frequency may be collected to help
improve our products. The collection and usage of all the information will be compliant with
<a href="https://www.google.com/policies/privacy">Google\'s Privacy Policy</a>.
</string>
<!-- Option name for including other IMEs in the language switch list [CHAR LIMIT=30] --> <!-- Option name for including other IMEs in the language switch list [CHAR LIMIT=30] -->
<string name="include_other_imes_in_language_switch_list">Switch to other input methods</string> <string name="include_other_imes_in_language_switch_list">Switch to other input methods</string>
<!-- Option summary for including other IMEs in the language switch list [CHAR LIMIT=65] --> <!-- Option summary for including other IMEs in the language switch list [CHAR LIMIT=65] -->

View File

@ -36,21 +36,14 @@
android:persistent="true" android:persistent="true"
android:disableDependentsState="false" /> android:disableDependentsState="false" />
<!-- Title will be set programmatically to embed application name -->
<CheckBoxPreference
android:key="pref_enable_metrics_logging"
android:summary="@string/enable_metrics_logging_summary"
android:defaultValue="true"
android:persistent="true" />
<!-- This preference (acts like a button) enables the user to initiate an one time sync. --> <!-- This preference (acts like a button) enables the user to initiate an one time sync. -->
<Preference android:key="pref_beanstalk" <Preference android:key="pref_sync_now"
android:persistent="false" android:persistent="false"
android:title="@string/sync_now_title" android:title="@string/sync_now_title"
android:dependency="pref_enable_cloud_sync" /> android:dependency="pref_enable_cloud_sync" />
<!-- This preference (acts like a button) enables the user to clear data from the cloud. --> <!-- This preference (acts like a button) enables the user to clear data from the cloud. -->
<Preference android:key="pref_beanstalk_clear_data" <Preference android:key="pref_clear_sync_data"
android:persistent="false" android:persistent="false"
android:title="@string/clear_sync_data_title" android:title="@string/clear_sync_data_title"
android:summary="@string/clear_sync_data_summary" android:summary="@string/clear_sync_data_summary"

View File

@ -23,8 +23,8 @@ import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener; import android.preference.Preference.OnPreferenceClickListener;
import android.text.TextUtils; import android.text.TextUtils;
@ -48,36 +48,50 @@ import javax.annotation.Nullable;
* <li> Privacy preferences </li> * <li> Privacy preferences </li>
*/ */
public final class AccountsSettingsFragment extends SubScreenFragment { public final class AccountsSettingsFragment extends SubScreenFragment {
private static final String PREF_SYNC_NOW = "pref_beanstalk"; private static final String PREF_ENABLE_SYNC_NOW = "pref_enable_cloud_sync";
private static final String PREF_CLEAR_SYNC_DATA = "pref_beanstalk_clear_data"; private static final String PREF_SYNC_NOW = "pref_sync_now";
private static final String PREF_CLEAR_SYNC_DATA = "pref_clear_sync_data";
static final String PREF_ACCCOUNT_SWITCHER = "account_switcher"; static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
private final DialogInterface.OnClickListener mAccountChangedListener = /**
new AccountChangedListener(); * Onclick listener for sync now pref.
private final Preference.OnPreferenceClickListener mSyncNowListener = new SyncNowListener(); */
private final Preference.OnPreferenceClickListener mClearSyncDataListener = private final Preference.OnPreferenceClickListener mSyncNowListener =
new ClearSyncDataListener(); new SyncNowListener();
/**
* Onclick listener for delete sync pref.
*/
private final Preference.OnPreferenceClickListener mDeleteSyncDataListener =
new DeleteSyncDataListener();
/**
* Onclick listener for enable sync pref.
*/
private final Preference.OnPreferenceClickListener mEnableSyncClickListener =
new EnableSyncClickListener();
/**
* Enable sync checkbox pref.
*/
private CheckBoxPreference mEnableSyncPreference;
/**
* Enable sync checkbox pref.
*/
private Preference mSyncNowPreference;
/**
* Clear sync data pref.
*/
private Preference mClearSyncDataPreference;
@Override @Override
public void onCreate(final Bundle icicle) { public void onCreate(final Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_screen_accounts); addPreferencesFromResource(R.xml.prefs_screen_accounts);
final Resources res = getResources();
if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
final Preference enableMetricsLogging =
findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
if (enableMetricsLogging != null) {
final String enableMetricsLoggingTitle = res.getString(
R.string.enable_metrics_logging, getApplicationName());
enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
}
} else {
removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
}
if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
removePreference(PREF_ACCCOUNT_SWITCHER); removePreference(PREF_ACCCOUNT_SWITCHER);
removePreference(PREF_ENABLE_CLOUD_SYNC); removePreference(PREF_ENABLE_CLOUD_SYNC);
@ -89,11 +103,14 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
removePreference(PREF_SYNC_NOW); removePreference(PREF_SYNC_NOW);
removePreference(PREF_CLEAR_SYNC_DATA); removePreference(PREF_CLEAR_SYNC_DATA);
} else { } else {
final Preference syncNowPreference = findPreference(PREF_SYNC_NOW); mEnableSyncPreference = (CheckBoxPreference) findPreference(PREF_ENABLE_SYNC_NOW);
syncNowPreference.setOnPreferenceClickListener(mSyncNowListener); mEnableSyncPreference.setOnPreferenceClickListener(mEnableSyncClickListener);
final Preference clearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA); mSyncNowPreference = findPreference(PREF_SYNC_NOW);
clearSyncDataPreference.setOnPreferenceClickListener(mClearSyncDataListener); mSyncNowPreference.setOnPreferenceClickListener(mSyncNowListener);
mClearSyncDataPreference = findPreference(PREF_CLEAR_SYNC_DATA);
mClearSyncDataPreference.setOnPreferenceClickListener(mDeleteSyncDataListener);
} }
} }
@ -106,15 +123,25 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
@Override @Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) { if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) {
refreshAccountAndDependentPreferences( refreshAccountAndDependentPreferences(prefs.getString(PREF_ACCOUNT_NAME, null));
prefs.getString(PREF_ACCOUNT_NAME, null));
} else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) { } else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) {
final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false); final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false);
AccountStateChangedListener.onSyncPreferenceChanged( mEnableSyncPreference = (CheckBoxPreference)findPreference(PREF_ENABLE_SYNC_NOW);
getSignedInAccountName(), syncEnabled); mEnableSyncPreference.setChecked(syncEnabled);
if (syncEnabled) {
mEnableSyncPreference.setSummary(R.string.cloud_sync_summary);
} else {
mEnableSyncPreference.setSummary(R.string.cloud_sync_summary_disabled);
}
AccountStateChangedListener.onSyncPreferenceChanged(getSignedInAccountName(),
syncEnabled);
} }
} }
/**
* Summarizes what account is being used and turns off dependent preferences if no account
* is currently selected.
*/
private void refreshAccountAndDependentPreferences(@Nullable final String currentAccount) { private void refreshAccountAndDependentPreferences(@Nullable final String currentAccount) {
if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
return; return;
@ -122,61 +149,33 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER); final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
if (currentAccount == null) { if (currentAccount == null) {
// No account is currently selected. // No account is currently selected; switch enable sync preference off.
accountSwitcher.setSummary(getString(R.string.no_accounts_selected)); accountSwitcher.setSummary(getString(R.string.no_accounts_selected));
// Disable the sync preference UI. mEnableSyncPreference.setChecked(false);
disableSyncPreference();
} else { } else {
// Set the currently selected account. // Set the currently selected account as the summary text.
accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount)); accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
// Enable the sync preference UI.
enableSyncPreference();
} }
// Set up onClick listener for the account picker preference.
final Context context = getActivity(); // Set up on click listener for the account picker preference.
final String[] accountsForLogin = LoginAccountUtils.getAccountsForLogin(context);
accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() { accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override @Override
public boolean onPreferenceClick(final Preference preference) { public boolean onPreferenceClick(final Preference preference) {
if (accountsForLogin.length == 0) { final String[] accountsForLogin =
// TODO: Handle account addition. LoginAccountUtils.getAccountsForLogin(getActivity());
Toast.makeText(getActivity(), getString(R.string.account_select_cancel), if (accountsForLogin.length == 0) {
Toast.LENGTH_SHORT).show(); // TODO: Handle account addition.
} else { Toast.makeText(getActivity(), getString(R.string.account_select_cancel),
createAccountPicker(accountsForLogin, currentAccount).show(); Toast.LENGTH_SHORT).show();
} else {
createAccountPicker(accountsForLogin, currentAccount,
new AccountChangedListener(null)).show();
}
return true;
} }
return true;
}
}); });
} }
/**
* Enables the Sync preference UI and updates its summary.
*/
private void enableSyncPreference() {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
final Preference syncPreference = findPreference(PREF_ENABLE_CLOUD_SYNC);
syncPreference.setEnabled(true);
syncPreference.setSummary(R.string.cloud_sync_summary);
}
/**
* Disables the Sync preference UI and updates its summary to indicate
* the fact that an account needs to be selected for sync.
*/
private void disableSyncPreference() {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
final Preference syncPreference = findPreference(PREF_ENABLE_CLOUD_SYNC);
syncPreference.setEnabled(false);
syncPreference.setSummary(R.string.cloud_sync_summary_disabled_signed_out);
}
@Nullable @Nullable
String getSignedInAccountName() { String getSignedInAccountName() {
return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null); return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
@ -188,14 +187,19 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
/** /**
* Creates an account picker dialog showing the given accounts in a list and selecting * Creates an account picker dialog showing the given accounts in a list and selecting
* the selected account by default. * the selected account by default. The list of accounts must not be null/empty.
* The list of accounts must not be null/empty.
* *
* Package-private for testing. * Package-private for testing.
*
* @param accounts list of accounts on the device.
* @param selectedAccount currently selected account
* @param positiveButtonClickListener listener that gets called when positive button is
* clicked
*/ */
@UsedForTesting @UsedForTesting
AlertDialog createAccountPicker(final String[] accounts, AlertDialog createAccountPicker(final String[] accounts,
final String selectedAccount) { final String selectedAccount,
final DialogInterface.OnClickListener positiveButtonClickListener) {
if (accounts == null || accounts.length == 0) { if (accounts == null || accounts.length == 0) {
throw new IllegalArgumentException("List of accounts must not be empty"); throw new IllegalArgumentException("List of accounts must not be empty");
} }
@ -216,10 +220,10 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.account_select_title) .setTitle(R.string.account_select_title)
.setSingleChoiceItems(accounts, index, null) .setSingleChoiceItems(accounts, index, null)
.setPositiveButton(R.string.account_select_ok, mAccountChangedListener) .setPositiveButton(R.string.account_select_ok, positiveButtonClickListener)
.setNegativeButton(R.string.account_select_cancel, null); .setNegativeButton(R.string.account_select_cancel, null);
if (isSignedIn) { if (isSignedIn) {
builder.setNeutralButton(R.string.account_select_sign_out, mAccountChangedListener); builder.setNeutralButton(R.string.account_select_sign_out, positiveButtonClickListener);
} }
return builder.create(); return builder.create();
} }
@ -229,6 +233,15 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
* Persists/removes the account to/from shared preferences and sets up sync if required. * Persists/removes the account to/from shared preferences and sets up sync if required.
*/ */
class AccountChangedListener implements DialogInterface.OnClickListener { class AccountChangedListener implements DialogInterface.OnClickListener {
/**
* Represents preference that should be changed based on account chosen.
*/
private CheckBoxPreference mDependentPreference;
AccountChangedListener(final CheckBoxPreference dependentPreference) {
mDependentPreference = dependentPreference;
}
@Override @Override
public void onClick(final DialogInterface dialog, final int which) { public void onClick(final DialogInterface dialog, final int which) {
final String oldAccount = getSignedInAccountName(); final String oldAccount = getSignedInAccountName();
@ -242,6 +255,9 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
.putString(PREF_ACCOUNT_NAME, newAccount) .putString(PREF_ACCOUNT_NAME, newAccount)
.apply(); .apply();
AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount); AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount);
if (mDependentPreference != null) {
mDependentPreference.setChecked(true);
}
break; break;
case DialogInterface.BUTTON_NEUTRAL: // Signed out case DialogInterface.BUTTON_NEUTRAL: // Signed out
AccountStateChangedListener.onAccountSignedOut(oldAccount); AccountStateChangedListener.onAccountSignedOut(oldAccount);
@ -268,7 +284,7 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
/** /**
* Listener that initiates the process of deleting user's data from the cloud. * Listener that initiates the process of deleting user's data from the cloud.
*/ */
class ClearSyncDataListener implements Preference.OnPreferenceClickListener { class DeleteSyncDataListener implements Preference.OnPreferenceClickListener {
@Override @Override
public boolean onPreferenceClick(final Preference preference) { public boolean onPreferenceClick(final Preference preference) {
final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity()) final AlertDialog confirmationDialog = new AlertDialog.Builder(getActivity())
@ -290,4 +306,43 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
return true; return true;
} }
} }
/**
* Listens to events when user clicks on "Enable sync" feature.
*/
class EnableSyncClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(final Preference preference) {
final CheckBoxPreference syncPreference = (CheckBoxPreference) preference;
if (syncPreference.isChecked()) {
// Uncheck for now.
syncPreference.setChecked(false);
// Show opt-in.
final AlertDialog optInDialog = new AlertDialog.Builder(getActivity())
.setTitle(R.string.cloud_sync_title)
.setMessage(R.string.cloud_sync_opt_in_text)
.setPositiveButton(R.string.account_select_ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog,
final int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
final Context context = getActivity();
final String[] accountsForLogin =
LoginAccountUtils.getAccountsForLogin(context);
createAccountPicker(accountsForLogin,
getSignedInAccountName(),
new AccountChangedListener(syncPreference))
.show();
}
}
})
.setNegativeButton(R.string.cloud_sync_cancel, null)
.create();
optInDialog.show();
}
return true;
}
}
} }

View File

@ -49,7 +49,7 @@ public class AccountsSettingsFragmentTests
final AccountsSettingsFragment fragment = final AccountsSettingsFragment fragment =
(AccountsSettingsFragment) getActivity().mFragment; (AccountsSettingsFragment) getActivity().mFragment;
try { try {
fragment.createAccountPicker(new String[0], null); fragment.createAccountPicker(new String[0], null, null /* listener */);
fail("Expected IllegalArgumentException, never thrown"); fail("Expected IllegalArgumentException, never thrown");
} catch (IllegalArgumentException expected) { } catch (IllegalArgumentException expected) {
// Expected. // Expected.
@ -76,7 +76,7 @@ public class AccountsSettingsFragmentTests
"2@example.com", "2@example.com",
"3@example.com", "3@example.com",
"4@example.com"}, "4@example.com"},
null); null, null /* positiveButtonListner */);
dialog.show(); dialog.show();
dialogHolder.mDialog = dialog; dialogHolder.mDialog = dialog;
latch.countDown(); latch.countDown();
@ -118,7 +118,7 @@ public class AccountsSettingsFragmentTests
"2@example.com", "2@example.com",
"3@example.com", "3@example.com",
"4@example.com"}, "4@example.com"},
"3@example.com"); "3@example.com", null /* positiveButtonListner */);
dialog.show(); dialog.show();
dialogHolder.mDialog = dialog; dialogHolder.mDialog = dialog;
latch.countDown(); latch.countDown();