1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,373 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.fxa.activities; 1.9 + 1.10 +import java.io.IOException; 1.11 +import java.util.Arrays; 1.12 +import java.util.HashSet; 1.13 +import java.util.Map; 1.14 +import java.util.Set; 1.15 + 1.16 +import org.mozilla.gecko.R; 1.17 +import org.mozilla.gecko.background.common.log.Logger; 1.18 +import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate; 1.19 +import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; 1.20 +import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; 1.21 +import org.mozilla.gecko.background.fxa.FxAccountUtils; 1.22 +import org.mozilla.gecko.background.fxa.PasswordStretcher; 1.23 +import org.mozilla.gecko.background.fxa.QuickPasswordStretcher; 1.24 +import org.mozilla.gecko.fxa.FxAccountConstants; 1.25 +import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay; 1.26 +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; 1.27 +import org.mozilla.gecko.fxa.login.Engaged; 1.28 +import org.mozilla.gecko.fxa.login.State; 1.29 +import org.mozilla.gecko.sync.SyncConfiguration; 1.30 +import org.mozilla.gecko.sync.setup.Constants; 1.31 +import org.mozilla.gecko.sync.setup.activities.ActivityUtils; 1.32 + 1.33 +import android.accounts.Account; 1.34 +import android.accounts.AccountManager; 1.35 +import android.content.Context; 1.36 +import android.content.Intent; 1.37 +import android.os.AsyncTask; 1.38 +import android.text.Editable; 1.39 +import android.text.TextWatcher; 1.40 +import android.text.method.PasswordTransformationMethod; 1.41 +import android.text.method.SingleLineTransformationMethod; 1.42 +import android.util.Patterns; 1.43 +import android.view.KeyEvent; 1.44 +import android.view.View; 1.45 +import android.view.View.OnClickListener; 1.46 +import android.view.View.OnFocusChangeListener; 1.47 +import android.widget.ArrayAdapter; 1.48 +import android.widget.AutoCompleteTextView; 1.49 +import android.widget.Button; 1.50 +import android.widget.EditText; 1.51 +import android.widget.ProgressBar; 1.52 +import android.widget.TextView; 1.53 +import android.widget.TextView.OnEditorActionListener; 1.54 + 1.55 +abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay { 1.56 + public FxAccountAbstractSetupActivity() { 1.57 + super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT); 1.58 + } 1.59 + 1.60 + protected FxAccountAbstractSetupActivity(int resume) { 1.61 + super(resume); 1.62 + } 1.63 + 1.64 + private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName(); 1.65 + 1.66 + protected int minimumPasswordLength = 8; 1.67 + 1.68 + protected AutoCompleteTextView emailEdit; 1.69 + protected EditText passwordEdit; 1.70 + protected Button showPasswordButton; 1.71 + protected TextView remoteErrorTextView; 1.72 + protected Button button; 1.73 + protected ProgressBar progressBar; 1.74 + 1.75 + protected void createShowPasswordButton() { 1.76 + showPasswordButton.setOnClickListener(new OnClickListener() { 1.77 + @SuppressWarnings("deprecation") 1.78 + @Override 1.79 + public void onClick(View v) { 1.80 + boolean isShown = passwordEdit.getTransformationMethod() instanceof SingleLineTransformationMethod; 1.81 + 1.82 + // Changing input type loses position in edit text; let's try to maintain it. 1.83 + int start = passwordEdit.getSelectionStart(); 1.84 + int stop = passwordEdit.getSelectionEnd(); 1.85 + 1.86 + if (isShown) { 1.87 + passwordEdit.setTransformationMethod(PasswordTransformationMethod.getInstance()); 1.88 + showPasswordButton.setText(R.string.fxaccount_password_show); 1.89 + showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_show_background)); 1.90 + showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_show_textcolor)); 1.91 + } else { 1.92 + passwordEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance()); 1.93 + showPasswordButton.setText(R.string.fxaccount_password_hide); 1.94 + showPasswordButton.setBackgroundDrawable(getResources().getDrawable(R.drawable.fxaccount_password_button_hide_background)); 1.95 + showPasswordButton.setTextColor(getResources().getColor(R.color.fxaccount_password_hide_textcolor)); 1.96 + } 1.97 + passwordEdit.setSelection(start, stop); 1.98 + } 1.99 + }); 1.100 + } 1.101 + 1.102 + protected void linkifyPolicy() { 1.103 + TextView policyView = (TextView) ensureFindViewById(null, R.id.policy, "policy links"); 1.104 + final String linkTerms = getString(R.string.fxaccount_link_tos); 1.105 + final String linkPrivacy = getString(R.string.fxaccount_link_pn); 1.106 + final String linkedTOS = "<a href=\"" + linkTerms + "\">" + getString(R.string.fxaccount_policy_linktos) + "</a>"; 1.107 + final String linkedPN = "<a href=\"" + linkPrivacy + "\">" + getString(R.string.fxaccount_policy_linkprivacy) + "</a>"; 1.108 + policyView.setText(getString(R.string.fxaccount_create_account_policy_text, linkedTOS, linkedPN)); 1.109 + final boolean underlineLinks = true; 1.110 + ActivityUtils.linkifyTextView(policyView, underlineLinks); 1.111 + } 1.112 + 1.113 + protected void hideRemoteError() { 1.114 + remoteErrorTextView.setVisibility(View.INVISIBLE); 1.115 + } 1.116 + 1.117 + protected void showRemoteError(Exception e, int defaultResourceId) { 1.118 + if (e instanceof IOException) { 1.119 + remoteErrorTextView.setText(R.string.fxaccount_remote_error_COULD_NOT_CONNECT); 1.120 + } else if (e instanceof FxAccountClientRemoteException) { 1.121 + showClientRemoteException((FxAccountClientRemoteException) e); 1.122 + } else { 1.123 + remoteErrorTextView.setText(defaultResourceId); 1.124 + } 1.125 + Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e); 1.126 + remoteErrorTextView.setVisibility(View.VISIBLE); 1.127 + } 1.128 + 1.129 + protected void showClientRemoteException(final FxAccountClientRemoteException e) { 1.130 + remoteErrorTextView.setText(e.getErrorMessageStringResource()); 1.131 + } 1.132 + 1.133 + protected void addListeners() { 1.134 + TextChangedListener textChangedListener = new TextChangedListener(); 1.135 + EditorActionListener editorActionListener = new EditorActionListener(); 1.136 + FocusChangeListener focusChangeListener = new FocusChangeListener(); 1.137 + 1.138 + emailEdit.addTextChangedListener(textChangedListener); 1.139 + emailEdit.setOnEditorActionListener(editorActionListener); 1.140 + emailEdit.setOnFocusChangeListener(focusChangeListener); 1.141 + passwordEdit.addTextChangedListener(textChangedListener); 1.142 + passwordEdit.setOnEditorActionListener(editorActionListener); 1.143 + passwordEdit.setOnFocusChangeListener(focusChangeListener); 1.144 + } 1.145 + 1.146 + protected class FocusChangeListener implements OnFocusChangeListener { 1.147 + @Override 1.148 + public void onFocusChange(View v, boolean hasFocus) { 1.149 + if (hasFocus) { 1.150 + return; 1.151 + } 1.152 + updateButtonState(); 1.153 + } 1.154 + } 1.155 + 1.156 + protected class EditorActionListener implements OnEditorActionListener { 1.157 + @Override 1.158 + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 1.159 + updateButtonState(); 1.160 + return false; 1.161 + } 1.162 + } 1.163 + 1.164 + protected class TextChangedListener implements TextWatcher { 1.165 + @Override 1.166 + public void afterTextChanged(Editable s) { 1.167 + updateButtonState(); 1.168 + } 1.169 + 1.170 + @Override 1.171 + public void beforeTextChanged(CharSequence s, int start, int count, int after) { 1.172 + // Do nothing. 1.173 + } 1.174 + 1.175 + @Override 1.176 + public void onTextChanged(CharSequence s, int start, int before, int count) { 1.177 + // Do nothing. 1.178 + } 1.179 + } 1.180 + 1.181 + protected boolean shouldButtonBeEnabled() { 1.182 + final String email = emailEdit.getText().toString(); 1.183 + final String password = passwordEdit.getText().toString(); 1.184 + 1.185 + boolean enabled = 1.186 + (email.length() > 0) && 1.187 + Patterns.EMAIL_ADDRESS.matcher(email).matches() && 1.188 + (password.length() >= minimumPasswordLength); 1.189 + return enabled; 1.190 + } 1.191 + 1.192 + protected boolean updateButtonState() { 1.193 + boolean enabled = shouldButtonBeEnabled(); 1.194 + if (!enabled) { 1.195 + // The user needs to do something before you can interact with the button; 1.196 + // presumably that interaction will fix whatever error is shown. 1.197 + hideRemoteError(); 1.198 + } 1.199 + if (enabled != button.isEnabled()) { 1.200 + Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button."); 1.201 + button.setEnabled(enabled); 1.202 + } 1.203 + return enabled; 1.204 + } 1.205 + 1.206 + @Override 1.207 + public void showProgress() { 1.208 + progressBar.setVisibility(View.VISIBLE); 1.209 + button.setVisibility(View.INVISIBLE); 1.210 + } 1.211 + 1.212 + @Override 1.213 + public void dismissProgress() { 1.214 + progressBar.setVisibility(View.INVISIBLE); 1.215 + button.setVisibility(View.VISIBLE); 1.216 + } 1.217 + 1.218 + public Intent makeSuccessIntent(String email, LoginResponse result) { 1.219 + Intent successIntent; 1.220 + if (result.verified) { 1.221 + successIntent = new Intent(this, FxAccountVerifiedAccountActivity.class); 1.222 + } else { 1.223 + successIntent = new Intent(this, FxAccountConfirmAccountActivity.class); 1.224 + } 1.225 + // Per http://stackoverflow.com/a/8992365, this triggers a known bug with 1.226 + // the soft keyboard not being shown for the started activity. Why, Android, why? 1.227 + successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 1.228 + return successIntent; 1.229 + } 1.230 + 1.231 + protected abstract class AddAccountDelegate implements RequestDelegate<LoginResponse> { 1.232 + public final String email; 1.233 + public final PasswordStretcher passwordStretcher; 1.234 + public final String serverURI; 1.235 + public final Map<String, Boolean> selectedEngines; 1.236 + 1.237 + public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) { 1.238 + this(email, passwordStretcher, serverURI, null); 1.239 + } 1.240 + 1.241 + public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI, Map<String, Boolean> selectedEngines) { 1.242 + if (email == null) { 1.243 + throw new IllegalArgumentException("email must not be null"); 1.244 + } 1.245 + if (passwordStretcher == null) { 1.246 + throw new IllegalArgumentException("passwordStretcher must not be null"); 1.247 + } 1.248 + if (serverURI == null) { 1.249 + throw new IllegalArgumentException("serverURI must not be null"); 1.250 + } 1.251 + this.email = email; 1.252 + this.passwordStretcher = passwordStretcher; 1.253 + this.serverURI = serverURI; 1.254 + // selectedEngines can be null, which means don't write 1.255 + // userSelectedEngines to prefs. This makes any created meta/global record 1.256 + // have the default set of engines to sync. 1.257 + this.selectedEngines = selectedEngines; 1.258 + } 1.259 + 1.260 + @Override 1.261 + public void handleSuccess(LoginResponse result) { 1.262 + Logger.info(LOG_TAG, "Got success response; adding Android account."); 1.263 + 1.264 + // We're on the UI thread, but it's okay to create the account here. 1.265 + AndroidFxAccount fxAccount; 1.266 + try { 1.267 + final String profile = Constants.DEFAULT_PROFILE; 1.268 + final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_ENDPOINT; 1.269 + // It is crucial that we use the email address provided by the server 1.270 + // (rather than whatever the user entered), because the user's keys are 1.271 + // wrapped and salted with the initial email they provided to 1.272 + // /create/account. Of course, we want to pass through what the user 1.273 + // entered locally as much as possible, so we create the Android account 1.274 + // with their entered email address, etc. 1.275 + // The passwordStretcher should have seen this email address before, so 1.276 + // we shouldn't be calculating the expensive stretch twice. 1.277 + byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8")); 1.278 + byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW); 1.279 + State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken); 1.280 + fxAccount = AndroidFxAccount.addAndroidAccount(getApplicationContext(), 1.281 + email, 1.282 + profile, 1.283 + serverURI, 1.284 + tokenServerURI, 1.285 + state); 1.286 + if (fxAccount == null) { 1.287 + throw new RuntimeException("Could not add Android account."); 1.288 + } 1.289 + 1.290 + if (selectedEngines != null) { 1.291 + Logger.info(LOG_TAG, "User has selected engines; storing to prefs."); 1.292 + SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines); 1.293 + } 1.294 + } catch (Exception e) { 1.295 + handleError(e); 1.296 + return; 1.297 + } 1.298 + 1.299 + // For great debugging. 1.300 + if (FxAccountConstants.LOG_PERSONAL_INFORMATION) { 1.301 + fxAccount.dump(); 1.302 + } 1.303 + 1.304 + // The GetStarted activity has called us and needs to return a result to the authenticator. 1.305 + final Intent intent = new Intent(); 1.306 + intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email); 1.307 + intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE); 1.308 + // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType); 1.309 + setResult(RESULT_OK, intent); 1.310 + 1.311 + // Show success activity depending on verification status. 1.312 + Intent successIntent = makeSuccessIntent(email, result); 1.313 + startActivity(successIntent); 1.314 + finish(); 1.315 + } 1.316 + } 1.317 + 1.318 + /** 1.319 + * Factory function that produces a new PasswordStretcher instance. 1.320 + * 1.321 + * @return PasswordStretcher instance. 1.322 + */ 1.323 + protected PasswordStretcher makePasswordStretcher(String password) { 1.324 + return new QuickPasswordStretcher(password); 1.325 + } 1.326 + 1.327 + protected abstract static class GetAccountsAsyncTask extends AsyncTask<Void, Void, Account[]> { 1.328 + protected final Context context; 1.329 + 1.330 + public GetAccountsAsyncTask(Context context) { 1.331 + super(); 1.332 + this.context = context; 1.333 + } 1.334 + 1.335 + @Override 1.336 + protected Account[] doInBackground(Void... params) { 1.337 + return AccountManager.get(context).getAccounts(); 1.338 + } 1.339 + } 1.340 + 1.341 + /** 1.342 + * This updates UI, so needs to be done on the foreground thread. 1.343 + */ 1.344 + protected void populateEmailAddressAutocomplete(Account[] accounts) { 1.345 + // First a set, since we don't want repeats. 1.346 + final Set<String> emails = new HashSet<String>(); 1.347 + for (Account account : accounts) { 1.348 + if (!Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 1.349 + continue; 1.350 + } 1.351 + emails.add(account.name); 1.352 + } 1.353 + 1.354 + // And then sorted in alphabetical order. 1.355 + final String[] sortedEmails = emails.toArray(new String[0]); 1.356 + Arrays.sort(sortedEmails); 1.357 + 1.358 + final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, sortedEmails); 1.359 + emailEdit.setAdapter(adapter); 1.360 + } 1.361 + 1.362 + @Override 1.363 + public void onResume() { 1.364 + super.onResume(); 1.365 + 1.366 + // Getting Accounts accesses databases on disk, so needs to be done on a 1.367 + // background thread. 1.368 + final GetAccountsAsyncTask task = new GetAccountsAsyncTask(this) { 1.369 + @Override 1.370 + public void onPostExecute(Account[] accounts) { 1.371 + populateEmailAddressAutocomplete(accounts); 1.372 + } 1.373 + }; 1.374 + task.execute(); 1.375 + } 1.376 +}