diff --git a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java index 4b0f2148bf7c..6da8184dbd25 100644 --- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java @@ -5,6 +5,7 @@ package org.mozilla.gecko.fxa.activities; import java.io.IOException; +import java.util.Map; import org.mozilla.gecko.R; import org.mozilla.gecko.background.common.log.Logger; @@ -18,6 +19,7 @@ import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.login.Engaged; import org.mozilla.gecko.fxa.login.State; +import org.mozilla.gecko.sync.SyncConfiguration; import org.mozilla.gecko.sync.setup.Constants; import android.accounts.AccountManager; @@ -58,6 +60,7 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc protected void createShowPasswordButton() { showPasswordButton.setOnClickListener(new OnClickListener() { + @SuppressWarnings("deprecation") @Override public void onClick(View v) { boolean isShown = 0 == (passwordEdit.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD); @@ -68,11 +71,11 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc passwordEdit.setSelection(start, stop); if (isShown) { showPasswordButton.setText(R.string.fxaccount_password_show); - showPasswordButton.setBackground(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background)); + showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background)); showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor)); } else { showPasswordButton.setText(R.string.fxaccount_password_hide); - showPasswordButton.setBackground(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background)); + showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background)); showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor)); } } @@ -199,11 +202,29 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc public final String email; public final PasswordStretcher passwordStretcher; public final String serverURI; + public final Map selectedEngines; public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) { + this(email, passwordStretcher, serverURI, null); + } + + public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI, Map selectedEngines) { + if (email == null) { + throw new IllegalArgumentException("email must not be null"); + } + if (passwordStretcher == null) { + throw new IllegalArgumentException("passwordStretcher must not be null"); + } + if (serverURI == null) { + throw new IllegalArgumentException("serverURI must not be null"); + } this.email = email; this.passwordStretcher = passwordStretcher; this.serverURI = serverURI; + // selectedEngines can be null, which means don't write + // userSelectedEngines to prefs. This makes any created meta/global record + // have the default set of engines to sync. + this.selectedEngines = selectedEngines; } @Override @@ -235,6 +256,11 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc if (fxAccount == null) { throw new RuntimeException("Could not add Android account."); } + + if (selectedEngines != null) { + Logger.info(LOG_TAG, "User has selected engines; storing to prefs."); + SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines); + } } catch (Exception e) { handleError(e); return; diff --git a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java index 24e7e8b50a68..748da0e1186a 100644 --- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java +++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java @@ -5,7 +5,9 @@ package org.mozilla.gecko.fxa.activities; import java.util.Calendar; +import java.util.HashMap; import java.util.LinkedList; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -32,7 +34,9 @@ import android.os.SystemClock; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; +import android.widget.CheckBox; import android.widget.EditText; +import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; @@ -46,6 +50,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi protected String[] yearItems; protected EditText yearEdit; + protected CheckBox chooseCheckBox; + + protected Map selectedEngines; /** * {@inheritDoc} @@ -73,12 +80,15 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view"); button = (Button) ensureFindViewById(null, R.id.button, "create account button"); progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar"); + chooseCheckBox = (CheckBox) ensureFindViewById(null, R.id.choose_what_to_sync_checkbox, "choose what to sync check box"); + selectedEngines = new HashMap(); createCreateAccountButton(); createYearEdit(); addListeners(); updateButtonState(); createShowPasswordButton(); + createChooseCheckBox(); View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link"); signInInsteadLink.setOnClickListener(new OnClickListener() { @@ -147,12 +157,12 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi }); } - public void createAccount(String email, String password) { + public void createAccount(String email, String password, Map engines) { String serverURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT; PasswordStretcher passwordStretcher = new QuickPasswordStretcher(password); // This delegate creates a new Android account on success, opens the // appropriate "success!" activity, and finishes this activity. - RequestDelegate delegate = new AddAccountDelegate(email, passwordStretcher, serverURI) { + RequestDelegate delegate = new AddAccountDelegate(email, passwordStretcher, serverURI, engines) { @Override public void handleError(Exception e) { showRemoteError(e, R.string.fxaccount_create_account_unknown_error); @@ -189,9 +199,13 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi } final String email = emailEdit.getText().toString(); final String password = passwordEdit.getText().toString(); + // Only include selected engines if the user currently has the option checked. + final Map engines = chooseCheckBox.isChecked() + ? selectedEngines + : null; if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) { FxAccountConstants.pii(LOG_TAG, "Passed age check."); - createAccount(email, password); + createAccount(email, password, engines); } else { FxAccountConstants.pii(LOG_TAG, "Failed age check!"); FxAccountAgeLockoutHelper.lockOut(SystemClock.elapsedRealtime()); @@ -201,4 +215,91 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi } }); } + + /** + * The "Choose what to sync" checkbox pops up a multi-choice dialog when it is + * unchecked. It toggles to unchecked from checked. + */ + protected void createChooseCheckBox() { + final int INDEX_BOOKMARKS = 0; + final int INDEX_HISTORY = 1; + final int INDEX_TABS = 2; + final int INDEX_PASSWORDS = 3; + final int NUMBER_OF_ENGINES = 4; + + final String items[] = new String[NUMBER_OF_ENGINES]; + final boolean checkedItems[] = new boolean[NUMBER_OF_ENGINES]; + items[INDEX_BOOKMARKS] = getResources().getString(R.string.fxaccount_status_bookmarks); + items[INDEX_HISTORY] = getResources().getString(R.string.fxaccount_status_history); + items[INDEX_TABS] = getResources().getString(R.string.fxaccount_status_tabs); + items[INDEX_PASSWORDS] = getResources().getString(R.string.fxaccount_status_passwords); + // Default to everything checked. + for (int i = 0; i < NUMBER_OF_ENGINES; i++) { + checkedItems[i] = true; + } + + final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which != DialogInterface.BUTTON_POSITIVE) { + Logger.debug(LOG_TAG, "onClick: not button positive, unchecking."); + chooseCheckBox.setChecked(false); + return; + } + // We only check the box on success. + Logger.debug(LOG_TAG, "onClick: button positive, checking."); + chooseCheckBox.setChecked(true); + // And then remember for future use. + ListView selectionsList = ((AlertDialog) dialog).getListView(); + for (int i = 0; i < NUMBER_OF_ENGINES; i++) { + checkedItems[i] = selectionsList.isItemChecked(i); + } + selectedEngines.put("bookmarks", checkedItems[INDEX_BOOKMARKS]); + selectedEngines.put("history", checkedItems[INDEX_HISTORY]); + selectedEngines.put("tabs", checkedItems[INDEX_TABS]); + selectedEngines.put("passwords", checkedItems[INDEX_PASSWORDS]); + FxAccountConstants.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString()); + } + }; + + final DialogInterface.OnMultiChoiceClickListener multiChoiceClickListener = new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + // Display multi-selection clicks in UI. + ListView selectionsList = ((AlertDialog) dialog).getListView(); + selectionsList.setItemChecked(which, isChecked); + } + }; + + final AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle(R.string.fxaccount_create_account_choose_what_to_sync) + .setIcon(R.drawable.icon) + .setMultiChoiceItems(items, checkedItems, multiChoiceClickListener) + .setPositiveButton(android.R.string.ok, clickListener) + .setNegativeButton(android.R.string.cancel, clickListener) + .create(); + + dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + Logger.debug(LOG_TAG, "onCancel: unchecking."); + chooseCheckBox.setChecked(false); + } + }); + + chooseCheckBox.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // There appears to be no way to stop Android interpreting the click + // first. So, if the user clicked on an unchecked box, it's checked by + // the time we get here. + if (!chooseCheckBox.isChecked()) { + Logger.debug(LOG_TAG, "onClick: was checked, not showing dialog."); + return; + } + Logger.debug(LOG_TAG, "onClick: was unchecked, showing dialog."); + dialog.show(); + } + }); + } } diff --git a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java index 9935583a0d40..0c46072244b3 100644 --- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java +++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java @@ -22,6 +22,7 @@ import org.mozilla.gecko.sync.Utils; import android.accounts.Account; import android.accounts.AccountManager; import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; /** @@ -239,6 +240,10 @@ public class AndroidFxAccount { return Utils.getPrefsPath(product, username, serverURLThing, profile, version); } + public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException { + return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE); + } + /** * Extract a JSON dictionary of the string values associated to this account. *

@@ -315,7 +320,7 @@ public class AndroidFxAccount { } public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException { - context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE).edit().clear().commit(); + getSyncPrefs().edit().clear().commit(); } public void enableSyncing() { diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index 49d5ca58ff1a..476b3f88ae0c 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -35,7 +35,6 @@ import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; import org.mozilla.gecko.sync.SyncConfiguration; -import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.crypto.KeyBundle; import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback; import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; @@ -219,7 +218,6 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { protected void syncWithAssertion(final String audience, final String assertion, URI tokenServerEndpointURI, - final String prefsPath, final SharedPreferences sharedPrefs, final KeyBundle syncKeyBundle, final String clientState, @@ -332,10 +330,8 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { return; } - final String prefsPath = fxAccount.getSyncPrefsPath(); - // This will be the same chunk of SharedPreferences that GlobalSession/SyncConfiguration will later create. - final SharedPreferences sharedPrefs = context.getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE); + final SharedPreferences sharedPrefs = fxAccount.getSyncPrefs(); final String audience = fxAccount.getAudience(); final String authServerEndpoint = fxAccount.getAccountServerURI(); @@ -389,7 +385,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { now + skewHandler.getSkewInMillis(), this.getAssertionDurationInMilliseconds()); final BaseGlobalSessionCallback sessionCallback = new SessionCallback(syncDelegate); - syncWithAssertion(audience, assertion, tokenServerEndpointURI, prefsPath, sharedPrefs, married.getSyncKeyBundle(), married.getClientState(), sessionCallback); + syncWithAssertion(audience, assertion, tokenServerEndpointURI, sharedPrefs, married.getSyncKeyBundle(), married.getClientState(), sessionCallback); } catch (Exception e) { syncDelegate.handleError(e); return; diff --git a/mobile/android/base/sync/GlobalSession.java b/mobile/android/base/sync/GlobalSession.java index 19f5e56d7379..be2863094e64 100644 --- a/mobile/android/base/sync/GlobalSession.java +++ b/mobile/android/base/sync/GlobalSession.java @@ -917,7 +917,29 @@ public class GlobalSession implements PrefsSource, HttpResponseObserver { return config.enabledEngineNames; } - return SyncConfiguration.validEngineNames(); + // These are the default set of engine names. + Set validEngineNames = SyncConfiguration.validEngineNames(); + + // If the user hasn't set any selected engines, that's okay -- default to + // everything. + if (config.userSelectedEngines == null) { + return validEngineNames; + } + + // userSelectedEngines has keys that are engine names, and boolean values + // corresponding to whether the user asked for the engine to sync or not. If + // an engine is not present, that means the user didn't change its sync + // setting. Since we default to everything on, that means the user didn't + // turn it off; therefore, it's included in the set of engines to sync. + Set validAndSelectedEngineNames = new HashSet(); + for (String engineName : validEngineNames) { + if (config.userSelectedEngines.containsKey(engineName) && + !config.userSelectedEngines.get(engineName)) { + continue; + } + validAndSelectedEngineNames.add(engineName); + } + return validAndSelectedEngineNames; } /** diff --git a/mobile/android/base/tokenserver/TokenServerClient.java b/mobile/android/base/tokenserver/TokenServerClient.java index d366c8e6ec22..e13d0fe3c31d 100644 --- a/mobile/android/base/tokenserver/TokenServerClient.java +++ b/mobile/android/base/tokenserver/TokenServerClient.java @@ -112,7 +112,7 @@ public class TokenServerClient { // Responses should *always* be JSON, even in the case of 4xx and 5xx // errors. If we don't see JSON, the server is likely very unhappy. String contentType = response.getEntity().getContentType().getValue(); - if (contentType != "application/json" && !contentType.startsWith("application/json;")) { + if (!contentType.equals("application/json") && !contentType.startsWith("application/json;")) { Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " + contentType + ". Misconfigured server?"); throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");