Merge f-t to m-c, a=merge

This commit is contained in:
Phil Ringnalda 2014-11-29 09:02:17 -08:00
commit f713acf7ea
40 changed files with 1089 additions and 191 deletions

View File

@ -838,10 +838,13 @@ sync_java_files = [
'fxa/AccountLoader.java',
'fxa/activities/FxAccountAbstractActivity.java',
'fxa/activities/FxAccountAbstractSetupActivity.java',
'fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java',
'fxa/activities/FxAccountConfirmAccountActivity.java',
'fxa/activities/FxAccountCreateAccountActivity.java',
'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
'fxa/activities/FxAccountFinishMigratingActivity.java',
'fxa/activities/FxAccountGetStartedActivity.java',
'fxa/activities/FxAccountMigrationFinishedActivity.java',
'fxa/activities/FxAccountSignInActivity.java',
'fxa/activities/FxAccountStatusActivity.java',
'fxa/activities/FxAccountStatusFragment.java',
@ -861,6 +864,7 @@ sync_java_files = [
'fxa/login/FxAccountLoginStateMachine.java',
'fxa/login/FxAccountLoginTransition.java',
'fxa/login/Married.java',
'fxa/login/MigratedFromSync11.java',
'fxa/login/Separated.java',
'fxa/login/State.java',
'fxa/login/StateFactory.java',
@ -879,6 +883,7 @@ sync_java_files = [
'fxa/tasks/FxAccountCreateAccountTask.java',
'fxa/tasks/FxAccountSetupTask.java',
'fxa/tasks/FxAccountSignInTask.java',
'fxa/tasks/FxAccountUnlockCodeResender.java',
'sync/AlreadySyncingException.java',
'sync/BackoffHandler.java',
'sync/BadRequiredFieldJSONException.java',

View File

@ -17,4 +17,5 @@ public interface FxAccountClient {
public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
public void resendCode(byte[] sessionToken, RequestDelegate<Void> delegate);
public void resendUnlockCode(byte[] emailUTF8, RequestDelegate<Void> delegate);
}

View File

@ -770,4 +770,46 @@ public class FxAccountClient10 {
};
post(resource, new JSONObject(), delegate);
}
/**
* Request a fresh unlock code be sent to the account email.
* <p>
* Since the account can be locked before the device can connect to it, the
* only reasonable identifier is the account email. Since the account is
* locked out, this request is un-authenticated.
*
* @param emailUTF8
* identifying account.
* @param delegate
* to invoke callbacks.
*/
@SuppressWarnings("unchecked")
public void resendUnlockCode(final byte[] emailUTF8, final RequestDelegate<Void> delegate) {
final BaseResource resource;
final JSONObject body = new JSONObject();
try {
resource = new BaseResource(new URI(serverURI + "account/unlock/resend_code"));
body.put("email", new String(emailUTF8, "UTF-8"));
} catch (URISyntaxException e) {
invokeHandleError(delegate, e);
return;
} catch (UnsupportedEncodingException e) {
invokeHandleError(delegate, e);
return;
}
resource.delegate = new ResourceDelegate<Void>(resource, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
delegate.handleSuccess(null);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
post(resource, body, delegate);
}
}

View File

@ -96,6 +96,10 @@ public class FxAccountClientException extends Exception {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE;
}
public boolean isAccountLocked() {
return apiErrorNumber == FxAccountRemoteError.ACCOUNT_LOCKED;
}
public int getErrorMessageStringResource() {
if (isUpgradeRequired()) {
return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
@ -111,6 +115,8 @@ public class FxAccountClientException extends Exception {
return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
} else if (isServerUnavailable()) {
return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
} else if (isAccountLocked()) {
return R.string.fxaccount_remote_error_ACCOUNT_LOCKED;
} else {
return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
}

View File

@ -16,7 +16,6 @@ public interface FxAccountRemoteError {
public static final int INVALID_REQUEST_SIGNATURE = 109;
public static final int INVALID_AUTHENTICATION_TOKEN = 110;
public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
public static final int INVALID_AUTHENTICATION_NONCE = 115;
public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
public static final int REQUEST_BODY_TOO_LARGE = 113;
public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
@ -26,6 +25,7 @@ public interface FxAccountRemoteError {
public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
public static final int INCORRECT_EMAIL_CASE = 120;
public static final int ACCOUNT_LOCKED = 121;
public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
public static final int UNKNOWN_ERROR = 999;
}

View File

@ -104,6 +104,8 @@ public class Distribution {
// Corresponds to the high value in Histograms.json.
private static final long MAX_DOWNLOAD_TIME_MSEC = 40000; // 40 seconds.
// Wait just a little while for the system to send a referrer intent after install.
private static final long DELAY_WAIT_FOR_REFERRER_MSEC = 400;
/**
@ -385,7 +387,16 @@ public class Distribution {
*/
private boolean checkIntentDistribution() {
if (referrer == null) {
return false;
// Wait a predetermined time and try again.
// Just block the thread, because it's the simplest solution.
try {
Thread.sleep(DELAY_WAIT_FOR_REFERRER_MSEC);
} catch (InterruptedException e) {
// Good enough.
}
if (referrer == null) {
return false;
}
}
URI uri = getReferredDistribution(referrer);

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.fxa.activities;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
@ -23,8 +24,10 @@ 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.fxa.tasks.FxAccountSetupTask.ProgressDisplay;
import org.mozilla.gecko.fxa.tasks.FxAccountUnlockCodeResender;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
@ -35,9 +38,12 @@ import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.method.PasswordTransformationMethod;
import android.text.method.SingleLineTransformationMethod;
import android.text.style.ClickableSpan;
import android.util.Patterns;
import android.view.KeyEvent;
import android.view.View;
@ -154,7 +160,35 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
}
protected void showClientRemoteException(final FxAccountClientRemoteException e) {
remoteErrorTextView.setText(e.getErrorMessageStringResource());
if (!e.isAccountLocked()) {
remoteErrorTextView.setText(e.getErrorMessageStringResource());
return;
}
// This horrible bit of special-casing is because we want this error message
// to contain a clickable, extra chunk of text, but we don't want to pollute
// the exception class with Android specifics.
final int messageId = e.getErrorMessageStringResource();
final int clickableId = R.string.fxaccount_resend_unlock_code_button_label;
final Spannable span = Utils.interpolateClickableSpan(this, messageId, clickableId, new ClickableSpan() {
@Override
public void onClick(View widget) {
// It would be best to capture the email address sent to the server
// and use it here, but this will do for now. If the user modifies
// the email address entered, the error text is hidden, so sending a
// changed email address would be the result of an unusual race.
final String email = emailEdit.getText().toString();
byte[] emailUTF8 = null;
try {
emailUTF8 = email.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// It's okay, we'll fail in the code resender.
}
FxAccountUnlockCodeResender.resendUnlockCode(FxAccountAbstractSetupActivity.this, getAuthServerEndpoint(), emailUTF8);
}
});
remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
remoteErrorTextView.setText(span);
}
protected void addListeners() {

View File

@ -0,0 +1,181 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.activities;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FirefoxAccounts;
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.fxa.tasks.FxAccountSignInTask;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
/**
* Abstract activity which displays a screen for updating the local password.
*/
public abstract class FxAccountAbstractUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
protected static final String LOG_TAG = FxAccountAbstractUpdateCredentialsActivity.class.getSimpleName();
protected AndroidFxAccount fxAccount;
protected final int layoutResourceId;
public FxAccountAbstractUpdateCredentialsActivity(int layoutResourceId) {
// We want to share code with the other setup activities, but this activity
// doesn't create a new Android Account, it modifies an existing one. If you
// manage to get an account, and somehow be locked out too, we'll let you
// update it.
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
this.layoutResourceId = layoutResourceId;
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle icicle) {
Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
super.onCreate(icicle);
setContentView(layoutResourceId);
emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
button = (Button) ensureFindViewById(null, R.id.button, "update credentials");
progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
minimumPasswordLength = 1; // Minimal restriction on passwords entered to sign in.
createButton();
addListeners();
updateButtonState();
createShowPasswordButton();
emailEdit.setEnabled(false);
TextView view = (TextView) findViewById(R.id.forgot_password_link);
ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password);
updateFromIntentExtras();
}
protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
public final String email;
public final String serverURI;
public final PasswordStretcher passwordStretcher;
public UpdateCredentialsDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
this.email = email;
this.serverURI = serverURI;
this.passwordStretcher = passwordStretcher;
}
@Override
public void handleError(Exception e) {
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
if (e.isUpgradeRequired()) {
Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
final State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
// The status activity will say that the user needs to upgrade.
redirectToActivity(FxAccountStatusActivity.class);
return;
}
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}
@Override
public void handleSuccess(LoginResponse result) {
Logger.info(LOG_TAG, "Got success signing in.");
if (fxAccount == null) {
this.handleError(new IllegalStateException("fxAccount must not be null"));
return;
}
byte[] unwrapkB;
try {
// It is crucial that we use the email address provided by the server
// (rather than whatever the user entered), because the user's keys are
// wrapped and salted with the initial email they provided to
// /create/account. Of course, we want to pass through what the user
// entered locally as much as possible.
byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
} catch (Exception e) {
this.handleError(e);
return;
}
fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
fxAccount.requestSync(FirefoxAccounts.FORCE);
// For great debugging.
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
setResult(RESULT_OK);
// Maybe show success activity.
final Intent successIntent = makeSuccessIntent(email, result);
if (successIntent != null) {
startActivity(successIntent);
}
finish();
}
}
public void updateCredentials(String email, String password) {
String serverURI = fxAccount.getAccountServerURI();
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient client = new FxAccountClient20(serverURI, executor);
PasswordStretcher passwordStretcher = makePasswordStretcher(password);
try {
hideRemoteError();
RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, passwordStretcher, serverURI);
new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception updating credentials for account.", e);
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}
}
protected void createButton() {
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final String email = emailEdit.getText().toString();
final String password = passwordEdit.getText().toString();
updateCredentials(email, password);
}
});
}
}

View File

@ -7,7 +7,6 @@ package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
@ -141,9 +140,6 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i
case NeedsVerification:
// This is what we're here to handle.
break;
case NeedsPassword:
case NeedsUpgrade:
case None:
default:
// We're not in the right place! Redirect to status.
Logger.warn(LOG_TAG, "No need to verify Firefox Account that needs action " + neededAction.toString() +

View File

@ -22,6 +22,7 @@ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClient
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.tasks.FxAccountCreateAccountTask;
import org.mozilla.gecko.sync.Utils;
import android.app.AlertDialog;
import android.app.Dialog;
@ -30,7 +31,6 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.Spannable;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
@ -124,13 +124,10 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
// This horrible bit of special-casing is because we want this error message to
// contain a clickable, extra chunk of text, but we don't want to pollute
// the exception class with Android specifics.
final String clickablePart = getString(R.string.fxaccount_sign_in_button_label);
final String message = getString(e.getErrorMessageStringResource(), clickablePart);
final int clickableStart = message.lastIndexOf(clickablePart);
final int clickableEnd = clickableStart + clickablePart.length();
final int messageId = e.getErrorMessageStringResource();
final int clickableId = R.string.fxaccount_sign_in_button_label;
final Spannable span = Spannable.Factory.getInstance().newSpannable(message);
span.setSpan(new ClickableSpan() {
final Spannable span = Utils.interpolateClickableSpan(this, messageId, clickableId, new ClickableSpan() {
@Override
public void onClick(View widget) {
// Pass through the email address that already existed.
@ -143,7 +140,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
final Bundle extras = makeExtrasBundle(email, password);
startActivityInstead(FxAccountSignInActivity.class, CHILD_REQUEST_CODE, extras);
}
}, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
});
remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
remoteErrorTextView.setText(span);
}

View File

@ -0,0 +1,54 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import android.content.Intent;
/**
* Activity which displays a screen for inputting the password and finishing
* migrating to Firefox Accounts / Sync 1.5.
*/
public class FxAccountFinishMigratingActivity extends FxAccountAbstractUpdateCredentialsActivity {
protected static final String LOG_TAG = FxAccountFinishMigratingActivity.class.getSimpleName();
public FxAccountFinishMigratingActivity() {
super(R.layout.fxaccount_finish_migrating);
}
@Override
public void onResume() {
super.onResume();
this.fxAccount = getAndroidFxAccount();
if (fxAccount == null) {
Logger.warn(LOG_TAG, "Could not get Firefox Account.");
setResult(RESULT_CANCELED);
finish();
return;
}
final State state = fxAccount.getState();
if (state.getStateLabel() != StateLabel.MigratedFromSync11) {
Logger.warn(LOG_TAG, "Cannot finish migrating from Firefox Account in state: " + state.getStateLabel());
setResult(RESULT_CANCELED);
finish();
return;
}
emailEdit.setText(fxAccount.getEmail());
}
@Override
public Intent makeSuccessIntent(String email, LoginResponse result) {
final Intent successIntent = new Intent(this, FxAccountMigrationFinishedActivity.class);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
return successIntent;
}
}

View File

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
/**
* Activity which displays "Upgrade finished" success screen.
*/
public class FxAccountMigrationFinishedActivity extends FxAccountAbstractActivity {
private static final String LOG_TAG = FxAccountMigrationFinishedActivity.class.getSimpleName();
protected AndroidFxAccount fxAccount;
public FxAccountMigrationFinishedActivity() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle icicle) {
Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
super.onCreate(icicle);
setContentView(R.layout.fxaccount_migration_finished);
}
@Override
public void onResume() {
super.onResume();
this.fxAccount = getAndroidFxAccount();
if (fxAccount == null) {
Logger.warn(LOG_TAG, "Could not get Firefox Account.");
setResult(RESULT_CANCELED);
finish();
return;
}
final State state = fxAccount.getState();
if (state.getNeededAction() == Action.NeedsFinishMigrating) {
Logger.warn(LOG_TAG, "Firefox Account needs to finish migrating; not displaying migration finished activity.");
setResult(RESULT_CANCELED);
finish();
return;
}
final View backToBrowsingButton = ensureFindViewById(null, R.id.button, "back to browsing button");
backToBrowsingButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ActivityUtils.openURLInFennec(v.getContext(), null);
}
});
}
}

View File

@ -5,13 +5,12 @@
package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.LocaleAware.LocaleAwareFragmentActivity;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.LocaleAware.LocaleAwareActivity;
import org.mozilla.gecko.LocaleAware.LocaleAwareFragmentActivity;
import android.accounts.Account;
import android.accounts.AccountManager;

View File

@ -77,6 +77,7 @@ public class FxAccountStatusFragment
protected Preference needsVerificationPreference;
protected Preference needsMasterSyncAutomaticallyEnabledPreference;
protected Preference needsAccountEnabledPreference;
protected Preference needsFinishMigratingPreference;
protected PreferenceCategory syncCategory;
@ -138,6 +139,7 @@ public class FxAccountStatusFragment
needsVerificationPreference = ensureFindPreference("needs_verification");
needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled");
needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating");
syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
@ -157,6 +159,7 @@ public class FxAccountStatusFragment
needsPasswordPreference.setOnPreferenceClickListener(this);
needsVerificationPreference.setOnPreferenceClickListener(this);
needsAccountEnabledPreference.setOnPreferenceClickListener(this);
needsFinishMigratingPreference.setOnPreferenceClickListener(this);
bookmarksPreference.setOnPreferenceClickListener(this);
historyPreference.setOnPreferenceClickListener(this);
@ -204,6 +207,20 @@ public class FxAccountStatusFragment
return true;
}
if (preference == needsFinishMigratingPreference) {
final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class);
final Bundle extras = getExtrasForAccount();
if (extras != null) {
intent.putExtras(extras);
}
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(intent);
return true;
}
if (preference == needsVerificationPreference) {
FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
@ -280,6 +297,7 @@ public class FxAccountStatusFragment
this.needsVerificationPreference,
this.needsMasterSyncAutomaticallyEnabledPreference,
this.needsAccountEnabledPreference,
this.needsFinishMigratingPreference,
};
for (Preference errorPreference : errorPreferences) {
final boolean currentlyShown = null != findPreference(errorPreference.getKey());
@ -325,6 +343,12 @@ public class FxAccountStatusFragment
setCheckboxesEnabled(false);
}
protected void showNeedsFinishMigrating() {
syncCategory.setTitle(R.string.fxaccount_status_sync);
showOnlyOneErrorPreference(needsFinishMigratingPreference);
setCheckboxesEnabled(false);
}
protected void showConnected() {
syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
showOnlyOneErrorPreference(null);
@ -464,8 +488,12 @@ public class FxAccountStatusFragment
case NeedsVerification:
showNeedsVerification();
break;
default:
case NeedsFinishMigrating:
showNeedsFinishMigrating();
break;
case None:
showConnected();
break;
}
// We check for the master setting last, since it is not strictly
@ -703,6 +731,11 @@ public class FxAccountStatusFragment
State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
refresh();
} else if ("debug_migrated_from_sync11".equals(key)) {
Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password.");
State state = fxAccount.getState();
fxAccount.setState(state.makeMigratedFromSync11State(null));
refresh();
} else {
return false;
}
@ -729,7 +762,8 @@ public class FxAccountStatusFragment
"debug_force_sync",
"debug_forget_certificate",
"debug_require_password",
"debug_require_upgrade" };
"debug_require_upgrade",
"debug_migrated_from_sync11" };
for (String debugKey : debugKeys) {
final Preference button = ensureFindPreference(debugKey);
button.setTitle(debugKey); // Not very friendly, but this is for debugging only!

View File

@ -4,80 +4,22 @@
package org.mozilla.gecko.fxa.activities;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FirefoxAccounts;
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.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.content.Intent;
/**
* Activity which displays a screen for updating the local password.
*/
public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractUpdateCredentialsActivity {
protected static final String LOG_TAG = FxAccountUpdateCredentialsActivity.class.getSimpleName();
protected AndroidFxAccount fxAccount;
public FxAccountUpdateCredentialsActivity() {
// We want to share code with the other setup activities, but this activity
// doesn't create a new Android Account, it modifies an existing one. If you
// manage to get an account, and somehow be locked out too, we'll let you
// update it.
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle icicle) {
Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
super.onCreate(icicle);
setContentView(R.layout.fxaccount_update_credentials);
emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
button = (Button) ensureFindViewById(null, R.id.button, "update credentials");
progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
minimumPasswordLength = 1; // Minimal restriction on passwords entered to sign in.
createButton();
addListeners();
updateButtonState();
createShowPasswordButton();
emailEdit.setEnabled(false);
TextView view = (TextView) findViewById(R.id.forgot_password_link);
ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password);
updateFromIntentExtras();
super(R.layout.fxaccount_update_credentials);
}
@Override
@ -90,7 +32,7 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
finish();
return;
}
State state = fxAccount.getState();
final State state = fxAccount.getState();
if (state.getStateLabel() != StateLabel.Separated) {
Logger.warn(LOG_TAG, "Cannot update credentials from Firefox Account in state: " + state.getStateLabel());
setResult(RESULT_CANCELED);
@ -100,93 +42,11 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
emailEdit.setText(fxAccount.getEmail());
}
protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
public final String email;
public final String serverURI;
public final PasswordStretcher passwordStretcher;
public UpdateCredentialsDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
this.email = email;
this.serverURI = serverURI;
this.passwordStretcher = passwordStretcher;
}
@Override
public void handleError(Exception e) {
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
if (e.isUpgradeRequired()) {
Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
final State state = fxAccount.getState();
fxAccount.setState(state.makeDoghouseState());
// The status activity will say that the user needs to upgrade.
redirectToActivity(FxAccountStatusActivity.class);
return;
}
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}
@Override
public void handleSuccess(LoginResponse result) {
Logger.info(LOG_TAG, "Got success signing in.");
if (fxAccount == null) {
this.handleError(new IllegalStateException("fxAccount must not be null"));
return;
}
byte[] unwrapkB;
try {
// It is crucial that we use the email address provided by the server
// (rather than whatever the user entered), because the user's keys are
// wrapped and salted with the initial email they provided to
// /create/account. Of course, we want to pass through what the user
// entered locally as much as possible.
byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
} catch (Exception e) {
this.handleError(e);
return;
}
fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
fxAccount.requestSync(FirefoxAccounts.FORCE);
// For great debugging.
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
setResult(RESULT_OK);
finish();
}
}
public void updateCredentials(String email, String password) {
String serverURI = fxAccount.getAccountServerURI();
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient client = new FxAccountClient20(serverURI, executor);
PasswordStretcher passwordStretcher = makePasswordStretcher(password);
try {
hideRemoteError();
RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, passwordStretcher, serverURI);
new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception updating credentials for account.", e);
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
}
}
protected void createButton() {
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final String email = emailEdit.getText().toString();
final String password = passwordEdit.getText().toString();
updateCredentials(email, password);
}
});
@Override
public Intent makeSuccessIntent(String email, LoginResponse result) {
// We don't show anything after updating credentials. The updating Activity
// sets its result to OK and the user is returned to the previous task,
// which is often the Status Activity.
return null;
}
}

View File

@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
public class MigratedFromSync11 extends State {
public final String password;
public MigratedFromSync11(String email, String uid, boolean verified, String password) {
super(StateLabel.MigratedFromSync11, email, uid, verified);
// Null password is allowed.
this.password = password;
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new PasswordRequired(), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsFinishMigrating;
}
}

View File

@ -9,7 +9,7 @@ import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public abstract class State {
public static final long CURRENT_VERSION = 2L;
public static final long CURRENT_VERSION = 3L;
public enum StateLabel {
Engaged,
@ -17,12 +17,14 @@ public abstract class State {
Married,
Separated,
Doghouse,
MigratedFromSync11,
}
public enum Action {
NeedsUpgrade,
NeedsPassword,
NeedsVerification,
NeedsFinishMigrating,
None,
}
@ -60,6 +62,10 @@ public abstract class State {
return new Doghouse(email, uid, verified);
}
public State makeMigratedFromSync11State(String password) {
return new MigratedFromSync11(email, uid, verified, password);
}
public abstract void execute(ExecuteDelegate delegate);
public abstract Action getNeededAction();

View File

@ -54,8 +54,11 @@ public class StateFactory {
}
final int v = version.intValue();
if (v == 2) {
if (v == 3) {
// The most common case is the most recent version.
return fromJSONObjectV3(stateLabel, o);
}
if (v == 2) {
return fromJSONObjectV2(stateLabel, o);
}
if (v == 1) {
@ -134,6 +137,23 @@ public class StateFactory {
}
}
/**
* Exactly the same as {@link fromJSONObjectV2}, except that there's a new
* MigratedFromSyncV11 state.
*/
protected static State fromJSONObjectV3(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
switch (stateLabel) {
case MigratedFromSync11:
return new MigratedFromSync11(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"),
o.getString("password"));
default:
return fromJSONObjectV2(stateLabel, o);
}
}
protected static void logMigration(State from, State to) {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return;

View File

@ -8,6 +8,7 @@ import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity;
import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
@ -67,12 +68,21 @@ public class FxAccountNotificationManager {
BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context);
}
final String title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
final String text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
final String title;
final String text;
final Intent notificationIntent;
if (action == Action.NeedsFinishMigrating) {
title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title);
text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email);
notificationIntent = new Intent(context, FxAccountFinishMigratingActivity.class);
} else {
title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
notificationIntent = new Intent(context, FxAccountStatusActivity.class);
}
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
FxAccountUtils.pii(LOG_TAG, "And text: " + text);
final Intent notificationIntent = new Intent(context, FxAccountStatusActivity.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
final Builder builder = new NotificationCompat.Builder(context);

View File

@ -118,6 +118,7 @@ public class FxAccountSchedulePolicy implements SchedulePolicy {
switch (needed) {
case NeedsPassword:
case NeedsUpgrade:
case NeedsFinishMigrating:
requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
break;
case NeedsVerification:

View File

@ -0,0 +1,105 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.tasks;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import android.content.Context;
import android.os.AsyncTask;
import android.widget.Toast;
/**
* A helper class that provides a simple interface for requesting a Firefox
* Account unlock account email be (re)-sent.
*/
public class FxAccountUnlockCodeResender {
private static final String LOG_TAG = FxAccountUnlockCodeResender.class.getSimpleName();
private static class FxAccountUnlockCodeTask extends FxAccountSetupTask<Void> {
protected static final String LOG_TAG = FxAccountUnlockCodeTask.class.getSimpleName();
protected final byte[] emailUTF8;
public FxAccountUnlockCodeTask(Context context, byte[] emailUTF8, FxAccountClient client, RequestDelegate<Void> delegate) {
super(context, null, client, delegate);
this.emailUTF8 = emailUTF8;
}
@Override
protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
try {
client.resendUnlockCode(emailUTF8, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception signing in.", e);
delegate.handleError(e);
}
return null;
}
}
private static class ResendUnlockCodeDelegate implements RequestDelegate<Void> {
public final Context context;
public ResendUnlockCodeDelegate(Context context) {
this.context = context;
}
@Override
public void handleError(Exception e) {
Logger.warn(LOG_TAG, "Got exception requesting fresh unlock code; ignoring.", e);
Toast.makeText(context, R.string.fxaccount_unlock_code_not_sent, Toast.LENGTH_LONG).show();
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
handleError(e);
}
@Override
public void handleSuccess(Void result) {
Toast.makeText(context, R.string.fxaccount_unlock_code_sent, Toast.LENGTH_SHORT).show();
}
}
/**
* Resends the account unlock email, and displays an appropriate toast on both
* send success and failure. Note that because the underlying implementation
* uses {@link AsyncTask}, the provided context must be UI-capable and this
* method called from the UI thread.
*
* Note that it may actually be possible to run this (and the
* {@link AsyncTask}) method from a background thread - but this hasn't been
* tested.
*
* @param context
* A UI-capable Android context.
* @param authServerURI
* to send request to.
* @param emailUTF8
* bytes of email address identifying account; null indicates a local failure.
*/
public static void resendUnlockCode(Context context, String authServerURI, byte[] emailUTF8) {
RequestDelegate<Void> delegate = new ResendUnlockCodeDelegate(context);
if (emailUTF8 == null) {
delegate.handleError(new IllegalArgumentException("emailUTF8 must not be null"));
return;
}
final Executor executor = Executors.newSingleThreadExecutor();
final FxAccountClient client = new FxAccountClient20(authServerURI, executor);
new FxAccountUnlockCodeTask(context, emailUTF8, client, delegate).execute();
}
}

View File

@ -157,6 +157,8 @@ public class RemoteTabsPanel extends HomeFragment {
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
case NeedsUpgrade:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_upgrade);
case NeedsFinishMigrating:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_finish_migrating);
default:
// This should never happen, but we're confident we have a Firefox
// Account at this point, so let's show the needs password screen.

View File

@ -10,6 +10,7 @@ import java.util.Locale;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity;
import org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity;
import org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@ -90,7 +91,8 @@ public class RemoteTabsStaticFragment extends HomeFragment implements OnClickLis
R.id.remote_tabs_setup_old_sync_link,
R.id.remote_tabs_needs_verification_resend_email,
R.id.remote_tabs_needs_verification_help,
R.id.remote_tabs_needs_password_sign_in, }) {
R.id.remote_tabs_needs_password_sign_in,
R.id.remote_tabs_needs_finish_migrating_sign_in, }) {
maybeSetOnClickListener(view, resourceId);
}
}
@ -116,10 +118,13 @@ public class RemoteTabsStaticFragment extends HomeFragment implements OnClickLis
final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
mUrlOpenListener.onUrlOpen(CONFIRM_ACCOUNT_SUPPORT_URL, flags);
} else if (id == R.id.remote_tabs_needs_password_sign_in) {
// This Activity will redirect to the correct Activity as needed.
final Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} else if (id == R.id.remote_tabs_needs_finish_migrating_sign_in) {
final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}

View File

@ -429,6 +429,8 @@ size. -->
<!ENTITY home_remote_tabs_empty "Your tabs from other devices show up here.">
<!ENTITY home_remote_tabs_unable_to_connect "Unable to connect">
<!ENTITY home_remote_tabs_need_to_sign_in "Please sign in to reconnect your Firefox Account and continue syncing.">
<!ENTITY home_remote_tabs_need_to_finish_migrating "Your new Firefox Account is ready!">
<!ENTITY home_remote_tabs_trouble_verifying "Trouble verifying your account?">
<!ENTITY home_remote_tabs_need_to_verify "Please verify your Firefox Account to start syncing.">

View File

@ -170,6 +170,9 @@
<!ENTITY fxaccount_confirm_account_change_email 'Forget this email address?'>
<!ENTITY fxaccount_confirm_account_verification_link_sent2 'Verification email sent'>
<!ENTITY fxaccount_confirm_account_verification_link_not_sent2 'Couldn\&apos;t send verification email'>
<!ENTITY fxaccount_resend_unlock_code_button_label 'Resend unlock email'>
<!ENTITY fxaccount_unlock_code_sent 'Account unlock email sent'>
<!ENTITY fxaccount_unlock_code_not_sent 'Couldn\&apos;t send account unlock email'>
<!ENTITY fxaccount_sign_in_sub_header 'Sign in'>
<!ENTITY fxaccount_sign_in_button_label 'Sign in'>
@ -180,10 +183,16 @@
<!ENTITY fxaccount_account_verified_sub_header 'Account verified'>
<!ENTITY fxaccount_account_verified_description2 'Your data will begin syncing momentarily.'>
<!ENTITY fxaccount_migration_finished_header 'Upgrade finished'>
<!ENTITY fxaccount_update_credentials_header 'Sign in'>
<!ENTITY fxaccount_update_credentials_button_label 'Sign in'>
<!ENTITY fxaccount_update_credentials_unknown_error 'Could not sign in'>
<!ENTITY fxaccount_finish_migrating_header 'Sign in to finish upgrading'>
<!ENTITY fxaccount_finish_migrating_button_label 'Finish upgrading'>
<!ENTITY fxaccount_finish_migrating_description 'Upgrading can transfer a lot of data. It\&apos;s best to be on a WiFi network.'>
<!ENTITY fxaccount_status_header2 'Firefox Account'>
<!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
<!ENTITY fxaccount_status_auth_server 'Account server'>
@ -198,6 +207,7 @@
<!ENTITY fxaccount_status_needs_upgrade 'You need to upgrade &brandShortName; to sign in.'>
<!ENTITY fxaccount_status_needs_master_sync_automatically_enabled '&syncBrand.shortName.label; is set up, but not syncing automatically. Toggle “Auto-sync data” in Android Settings &gt; Data Usage.'>
<!ENTITY fxaccount_status_needs_account_enabled '&syncBrand.shortName.label; is set up, but not syncing automatically. Tap to start syncing.'>
<!ENTITY fxaccount_status_needs_finish_migrating 'Tap to sign in to your new Firefox Account.'>
<!ENTITY fxaccount_status_bookmarks 'Bookmarks'>
<!ENTITY fxaccount_status_history 'History'>
<!ENTITY fxaccount_status_passwords 'Passwords'>
@ -245,8 +255,14 @@
<!ENTITY fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD 'Server busy, try again soon'>
<!ENTITY fxaccount_remote_error_UNKNOWN_ERROR 'There was a problem'>
<!ENTITY fxaccount_remote_error_COULD_NOT_CONNECT 'Cannot connect to network'>
<!ENTITY fxaccount_remote_error_ACCOUNT_LOCKED 'Account is locked. &formatS1;'>
<!ENTITY fxaccount_sync_sign_in_error_notification_title2 '&syncBrand.shortName.label; is not connected'>
<!-- Localization note: the format string below will be replaced
with the Firefox Account's email address. -->
<!ENTITY fxaccount_sync_sign_in_error_notification_text2 'Tap to sign in as &formatS;'>
<!ENTITY fxaccount_sync_finish_migrating_notification_title 'Finish upgrading &syncBrand.shortName.label;?'>
<!-- Localization note: the format string below will be replaced
with the Firefox Account's email address. -->
<!ENTITY fxaccount_sync_finish_migrating_notification_text 'Tap to sign in as &formatS;'>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fillViewport="true" >
<LinearLayout
android:id="@+id/update_credentials_view"
style="@style/FxAccountMiddle" >
<LinearLayout style="@style/FxAccountSpacer" />
<TextView
style="@style/FxAccountHeaderItem"
android:text="@string/fxaccount_finish_migrating_header" />
<include layout="@layout/fxaccount_custom_server_view" />
<include layout="@layout/fxaccount_email_password_view" />
<TextView
style="@style/FxAccountTextItem"
android:layout_marginTop="10dp"
android:text="@string/fxaccount_finish_migrating_description" />
<TextView
android:id="@+id/remote_error"
style="@style/FxAccountErrorItem" />
<RelativeLayout style="@style/FxAccountButtonLayout" >
<ProgressBar
android:id="@+id/progress"
style="@style/FxAccountProgress" />
<Button
android:id="@+id/button"
style="@style/FxAccountButton"
android:text="@string/fxaccount_finish_migrating_button_label" />
</RelativeLayout>
<TextView
android:id="@+id/forgot_password_link"
style="@style/FxAccountLinkifiedItem"
android:layout_marginTop="10dp"
android:text="@string/fxaccount_sign_in_forgot_password" />
<LinearLayout style="@style/FxAccountSpacer" />
<ImageView
style="@style/FxAccountIcon"
android:contentDescription="@string/fxaccount_empty_contentDescription" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fillViewport="true" >
<LinearLayout style="@style/FxAccountMiddle" >
<TextView
style="@style/FxAccountHeaderItem"
android:text="@string/fxaccount_migration_finished_header" >
</TextView>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="45dp"
android:contentDescription="@string/fxaccount_empty_contentDescription"
android:src="@drawable/fxaccount_checkbox" >
</ImageView>
<TextView
style="@style/FxAccountTextItem"
android:layout_marginBottom="40dp"
android:text="@string/fxaccount_migration_finished_description"
android:textSize="18sp" />
<Button
android:id="@+id/button"
style="@style/FxAccountButton"
android:text="@string/fxaccount_back_to_browsing" />
<LinearLayout style="@style/FxAccountSpacer" />
<ImageView
style="@style/FxAccountIcon"
android:contentDescription="@string/fxaccount_empty_contentDescription" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout style="@style/RemoteTabsPanelFrame" >
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_finish_migrating_header" />
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_remote_tabs_need_to_finish_migrating" />
<Button
android:id="@+id/remote_tabs_needs_finish_migrating_sign_in"
style="@style/RemoteTabsPanelItem.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_finish_migrating_button_label" />
</LinearLayout>
</ScrollView>

View File

@ -54,6 +54,13 @@
android:layout="@layout/fxaccount_status_error_preference"
android:persistent="false"
android:title="@string/fxaccount_status_needs_account_enabled" />
<Preference
android:editable="false"
android:icon="@drawable/fxaccount_sync_error"
android:key="needs_finish_migrating"
android:layout="@layout/fxaccount_status_error_preference"
android:persistent="false"
android:title="@string/fxaccount_status_needs_finish_migrating" />
<Preference
android:editable="false"
@ -125,6 +132,7 @@
<Preference android:key="debug_forget_certificate" />
<Preference android:key="debug_require_password" />
<Preference android:key="debug_require_upgrade" />
<Preference android:key="debug_migrated_from_sync11" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -369,6 +369,7 @@
<string name="home_remote_tabs_empty">&home_remote_tabs_empty;</string>
<string name="home_remote_tabs_unable_to_connect">&home_remote_tabs_unable_to_connect;</string>
<string name="home_remote_tabs_need_to_sign_in">&home_remote_tabs_need_to_sign_in;</string>
<string name="home_remote_tabs_need_to_finish_migrating">&home_remote_tabs_need_to_finish_migrating;</string>
<string name="home_remote_tabs_trouble_verifying">&home_remote_tabs_trouble_verifying;</string>
<string name="home_remote_tabs_need_to_verify">&home_remote_tabs_need_to_verify;</string>
<string name="home_remote_tabs_one_hidden_device">&home_remote_tabs_one_hidden_device;</string>

View File

@ -35,6 +35,9 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.ClickableSpan;
public class Utils {
@ -614,4 +617,26 @@ public class Utils {
}
return language + "-" + country;
}
/**
* Make a span with a clickable chunk of text interpolated in.
*
* @param context Android context.
* @param messageId of string containing clickable chunk.
* @param clickableId of string to make clickable.
* @param clickableSpan to activate on click.
* @return Spannable.
*/
public static Spannable interpolateClickableSpan(Context context, int messageId, int clickableId, ClickableSpan clickableSpan) {
// This horrible bit of special-casing is because we want this error message to
// contain a clickable, extra chunk of text, but we don't want to pollute
// the exception class with Android specifics.
final String clickablePart = context.getString(clickableId);
final String message = context.getString(messageId, clickablePart);
final int clickableStart = message.lastIndexOf(clickablePart);
final int clickableEnd = clickableStart + clickablePart.length();
final Spannable span = Spannable.Factory.getInstance().newSpannable(message);
span.setSpan(clickableSpan, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return span;
}
}

View File

@ -67,6 +67,21 @@
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountMigrationFinishedActivity"
android:configChanges="locale|layoutDirection"
android:noHistory="true"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountNotAllowedActivity"

View File

@ -157,6 +157,9 @@
<string name="fxaccount_confirm_account_change_email">&fxaccount_confirm_account_change_email;</string>
<string name="fxaccount_confirm_account_verification_link_sent">&fxaccount_confirm_account_verification_link_sent2;</string>
<string name="fxaccount_confirm_account_verification_link_not_sent">&fxaccount_confirm_account_verification_link_not_sent2;</string>
<string name="fxaccount_resend_unlock_code_button_label">&fxaccount_resend_unlock_code_button_label;</string>
<string name="fxaccount_unlock_code_sent">&fxaccount_unlock_code_sent;</string>
<string name="fxaccount_unlock_code_not_sent">&fxaccount_unlock_code_not_sent;</string>
<string name="fxaccount_sign_in_sub_header">&fxaccount_sign_in_sub_header;</string>
<string name="fxaccount_sign_in_button_label">&fxaccount_sign_in_button_label;</string>
@ -167,10 +170,17 @@
<string name="fxaccount_account_verified_sub_header">&fxaccount_account_verified_sub_header;</string>
<string name="fxaccount_account_verified_description">&fxaccount_account_verified_description2;</string>
<string name="fxaccount_migration_finished_header">&fxaccount_migration_finished_header;</string>
<string name="fxaccount_migration_finished_description">&fxaccount_account_verified_description2;</string>
<string name="fxaccount_update_credentials_header">&fxaccount_update_credentials_header;</string>
<string name="fxaccount_update_credentials_button_label">&fxaccount_update_credentials_button_label;</string>
<string name="fxaccount_update_credentials_unknown_error">&fxaccount_update_credentials_unknown_error;</string>
<string name="fxaccount_finish_migrating_header">&fxaccount_finish_migrating_header;</string>
<string name="fxaccount_finish_migrating_button_label">&fxaccount_finish_migrating_button_label;</string>
<string name="fxaccount_finish_migrating_description">&fxaccount_finish_migrating_description;</string>
<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
<string name="fxaccount_status_header">&fxaccount_status_header2;</string>
<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
@ -187,6 +197,7 @@
<string name="fxaccount_status_needs_upgrade">&fxaccount_status_needs_upgrade;</string>
<string name="fxaccount_status_needs_master_sync_automatically_enabled">&fxaccount_status_needs_master_sync_automatically_enabled;</string>
<string name="fxaccount_status_needs_account_enabled">&fxaccount_status_needs_account_enabled;</string>
<string name="fxaccount_status_needs_finish_migrating">&fxaccount_status_needs_finish_migrating;</string>
<string name="fxaccount_status_bookmarks">&fxaccount_status_bookmarks;</string>
<string name="fxaccount_status_history">&fxaccount_status_history;</string>
<string name="fxaccount_status_passwords">&fxaccount_status_passwords;</string>
@ -210,6 +221,7 @@
<string name="fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD">&fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;</string>
<string name="fxaccount_remote_error_UNKNOWN_ERROR">&fxaccount_remote_error_UNKNOWN_ERROR;</string>
<string name="fxaccount_remote_error_COULD_NOT_CONNECT">&fxaccount_remote_error_COULD_NOT_CONNECT;</string>
<string name="fxaccount_remote_error_ACCOUNT_LOCKED">&fxaccount_remote_error_ACCOUNT_LOCKED;</string>
<string name="fxaccount_sync_sign_in_error_notification_title">&fxaccount_sync_sign_in_error_notification_title2;</string>
<string name="fxaccount_sync_sign_in_error_notification_text">&fxaccount_sync_sign_in_error_notification_text2;</string>
@ -219,3 +231,6 @@
<string name="fxaccount_remove_account_dialog_message">&fxaccount_remove_account_dialog_message;</string>
<string name="fxaccount_remove_account_toast">&fxaccount_remove_account_toast;</string>
<string name="fxaccount_remove_account_menu_item">&fxaccount_remove_account_menu_item;</string>
<string name="fxaccount_sync_finish_migrating_notification_title">&fxaccount_sync_finish_migrating_notification_title;</string>
<string name="fxaccount_sync_finish_migrating_notification_text">&fxaccount_sync_finish_migrating_notification_text;</string>

View File

@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { RootActor } = devtools.require("devtools/server/actors/root");
const { BrowserTabList } = devtools.require("devtools/server/actors/webbrowser");
/**
* xpcshell-test (XPCST) specific actors.
*
*/
/**
* Construct a root actor appropriate for use in a server running xpcshell
* tests. <snip boilerplate> :)
*/
function createRootActor(connection)
{
let parameters = {
tabList: new XPCSTTabList(connection),
globalActorFactories: DebuggerServer.globalActorFactories,
onShutdown() {
// If the user never switches to the "debugger" tab we might get a
// shutdown before we've attached.
Services.obs.notifyObservers(null, "xpcshell-test-devtools-shutdown", null);
}
};
return new RootActor(connection, parameters);
}
/**
* A "stub" TabList implementation that provides no tabs.
*/
function XPCSTTabList(connection)
{
BrowserTabList.call(this, connection);
}
XPCSTTabList.prototype = Object.create(BrowserTabList.prototype);
XPCSTTabList.prototype.constructor = XPCSTTabList;
XPCSTTabList.prototype.getList = function() {
return Promise.resolve([]);
};

View File

@ -336,7 +336,101 @@ function _register_modules_protocol_handler() {
protocolHandler.setSubstitution("testing-common", modulesURI);
}
function _initDebugging(port) {
let prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
// Always allow remote debugging.
prefs.setBoolPref("devtools.debugger.remote-enabled", true);
// for debugging-the-debugging, let an env var cause log spew.
let env = Components.classes["@mozilla.org/process/environment;1"]
.getService(Components.interfaces.nsIEnvironment);
if (env.get("DEVTOOLS_DEBUGGER_LOG")) {
prefs.setBoolPref("devtools.debugger.log", true);
}
if (env.get("DEVTOOLS_DEBUGGER_LOG_VERBOSE")) {
prefs.setBoolPref("devtools.debugger.log.verbose", true);
}
let {DebuggerServer} = Components.utils.import('resource://gre/modules/devtools/dbg-server.jsm', {});
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
DebuggerServer.addActors("resource://testing-common/dbg-actors.js");
// An observer notification that tells us when we can "resume" script
// execution.
let obsSvc = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
let initialized = false;
const TOPICS = ["devtools-thread-resumed", "xpcshell-test-devtools-shutdown"];
let observe = function(subject, topic, data) {
switch (topic) {
case "devtools-thread-resumed":
// Exceptions in here aren't reported and block the debugger from
// resuming, so...
try {
// Add a breakpoint for the first line in our test files.
let threadActor = subject.wrappedJSObject;
let location = { line: 1 };
for (let file of _TEST_FILE) {
let sourceActor = threadActor.sources.source({originalUrl: file});
sourceActor.createAndStoreBreakpoint(location);
}
} catch (ex) {
do_print("Failed to initialize breakpoints: " + ex + "\n" + ex.stack);
}
break;
case "xpcshell-test-devtools-shutdown":
// the debugger has shutdown before we got a resume event - nothing
// special to do here.
break;
}
initialized = true;
for (let topicToRemove of TOPICS) {
obsSvc.removeObserver(observe, topicToRemove);
}
};
for (let topic of TOPICS) {
obsSvc.addObserver(observe, topic, false);
}
do_print("");
do_print("*******************************************************************");
do_print("Waiting for the debugger to connect on port " + port)
do_print("")
do_print("To connect the debugger, open a Firefox instance, select 'Connect'");
do_print("from the Developer menu and specify the port as " + port);
do_print("*******************************************************************");
do_print("")
DebuggerServer.openListener(port);
// spin an event loop until the debugger connects.
let thr = Components.classes["@mozilla.org/thread-manager;1"]
.getService().currentThread;
while (!initialized) {
do_print("Still waiting for debugger to connect...");
thr.processNextEvent(true);
}
// NOTE: if you want to debug the harness itself, you can now add a 'debugger'
// statement anywhere and it will stop - but we've already added a breakpoint
// for the first line of the test scripts, so we just continue...
do_print("Debugger connected, starting test execution");
}
function _execute_test() {
// _JSDEBUGGER_PORT is dynamically defined by <runxpcshelltests.py>.
if (_JSDEBUGGER_PORT) {
try {
_initDebugging(_JSDEBUGGER_PORT);
} catch (ex) {
do_print("Failed to initialize debugging: " + ex + "\n" + ex.stack);
}
}
_register_protocol_handlers();
// Override idle service by default.
@ -1072,6 +1166,8 @@ function do_load_child_test_harness()
+ "const _HEAD_FILES=" + uneval(_HEAD_FILES) + "; "
+ "const _TAIL_FILES=" + uneval(_TAIL_FILES) + "; "
+ "const _TEST_NAME=" + uneval(_TEST_NAME) + "; "
// We'll need more magic to get the debugger working in the child
+ "const _JSDEBUGGER_PORT=0; "
+ "const _XPCSHELL_PROCESS='child';";
if (this._TESTING_MODULES_DIR) {

View File

@ -65,6 +65,7 @@ class XPCShellRunner(MozbuildObject):
def run_test(self, test_paths, interactive=False,
keep_going=False, sequential=False, shuffle=False,
debugger=None, debuggerArgs=None, debuggerInteractive=None,
jsDebugger=False, jsDebuggerPort=None,
rerun_failures=False, test_objects=None, verbose=False,
log=None,
# ignore parameters from other platforms' options
@ -83,6 +84,7 @@ class XPCShellRunner(MozbuildObject):
keep_going=keep_going, shuffle=shuffle, sequential=sequential,
debugger=debugger, debuggerArgs=debuggerArgs,
debuggerInteractive=debuggerInteractive,
jsDebugger=jsDebugger, jsDebuggerPort=jsDebuggerPort,
rerun_failures=rerun_failures,
verbose=verbose, log=log)
return
@ -113,6 +115,8 @@ class XPCShellRunner(MozbuildObject):
'debugger': debugger,
'debuggerArgs': debuggerArgs,
'debuggerInteractive': debuggerInteractive,
'jsDebugger': jsDebugger,
'jsDebuggerPort': jsDebuggerPort,
'rerun_failures': rerun_failures,
'manifest': manifest,
'verbose': verbose,
@ -125,6 +129,7 @@ class XPCShellRunner(MozbuildObject):
test_path=None, shuffle=False, interactive=False,
keep_going=False, sequential=False,
debugger=None, debuggerArgs=None, debuggerInteractive=None,
jsDebugger=False, jsDebuggerPort=None,
rerun_failures=False, verbose=False, log=None):
# Obtain a reference to the xpcshell test runner.
@ -161,6 +166,8 @@ class XPCShellRunner(MozbuildObject):
'debugger': debugger,
'debuggerArgs': debuggerArgs,
'debuggerInteractive': debuggerInteractive,
'jsDebugger': jsDebugger,
'jsDebuggerPort': jsDebuggerPort,
}
if test_path is not None:
@ -417,6 +424,13 @@ class MachCommands(MachCommandBase):
dest = "debuggerInteractive",
help = "prevents the test harness from redirecting "
"stdout and stderr for interactive debuggers")
@CommandArgument("--jsdebugger", dest="jsDebugger", action="store_true",
help="Waits for a devtools JS debugger to connect before "
"starting the test.")
@CommandArgument("--jsdebugger-port", dest="jsDebuggerPort",
type=int, default=6000,
help="The port to listen on for a debugger connection if "
"--jsdebugger is specified (default=6000).")
@CommandArgument('--interactive', '-i', action='store_true',
help='Open an xpcshell prompt before running tests.')
@CommandArgument('--keep-going', '-k', action='store_true',

View File

@ -9,3 +9,7 @@ TEST_DIRS += ['example']
PYTHON_UNIT_TESTS += [
'selftest.py',
]
TESTING_JS_MODULES += [
'dbg-actors.js',
]

View File

@ -19,7 +19,7 @@ import sys
import time
import traceback
from collections import deque
from collections import deque, namedtuple
from distutils import dir_util
from multiprocessing import cpu_count
from optparse import OptionParser
@ -111,6 +111,7 @@ class XPCShellTestThread(Thread):
self.xrePath = kwargs.get('xrePath')
self.testingModulesDir = kwargs.get('testingModulesDir')
self.debuggerInfo = kwargs.get('debuggerInfo')
self.jsDebuggerInfo = kwargs.get('jsDebuggerInfo')
self.pluginsPath = kwargs.get('pluginsPath')
self.httpdManifest = kwargs.get('httpdManifest')
self.httpdJSPath = kwargs.get('httpdJSPath')
@ -366,10 +367,15 @@ class XPCShellTestThread(Thread):
for f in headfiles])
cmdT = ", ".join(['"' + f.replace('\\', '/') + '"'
for f in tailfiles])
dbgport = 0 if self.jsDebuggerInfo is None else self.jsDebuggerInfo.port
return xpcscmd + \
['-e', 'const _SERVER_ADDR = "localhost"',
'-e', 'const _HEAD_FILES = [%s];' % cmdH,
'-e', 'const _TAIL_FILES = [%s];' % cmdT]
'-e', 'const _TAIL_FILES = [%s];' % cmdT,
'-e', 'const _JSDEBUGGER_PORT = %d;' % dbgport,
]
def getHeadAndTailFiles(self, test_object):
"""Obtain the list of head and tail files.
@ -632,7 +638,7 @@ class XPCShellTestThread(Thread):
testTimeoutInterval *= int(self.test_object['requesttimeoutfactor'])
testTimer = None
if not self.interactive and not self.debuggerInfo:
if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
testTimer.start()
@ -1004,7 +1010,8 @@ class XPCShellTests(object):
profileName=None, mozInfo=None, sequential=False, shuffle=False,
testsRootDir=None, testingModulesDir=None, pluginsPath=None,
testClass=XPCShellTestThread, failureManifest=None,
log=None, stream=None, **otherOptions):
log=None, stream=None, jsDebugger=False, jsDebuggerPort=0,
**otherOptions):
"""Run xpcshell tests.
|xpcshell|, is the xpcshell executable to use to run the tests.
@ -1075,6 +1082,12 @@ class XPCShellTests(object):
if debugger:
self.debuggerInfo = mozdebug.get_debugger_info(debugger, debuggerArgs, debuggerInteractive)
self.jsDebuggerInfo = None
if jsDebugger:
# A namedtuple let's us keep .port instead of ['port']
JSDebuggerInfo = namedtuple('JSDebuggerInfo', ['port'])
self.jsDebuggerInfo = JSDebuggerInfo(port=jsDebuggerPort)
self.xpcshell = xpcshell
self.xrePath = xrePath
self.appPath = appPath
@ -1161,6 +1174,7 @@ class XPCShellTests(object):
'xrePath': self.xrePath,
'testingModulesDir': self.testingModulesDir,
'debuggerInfo': self.debuggerInfo,
'jsDebuggerInfo': self.jsDebuggerInfo,
'pluginsPath': self.pluginsPath,
'httpdManifest': self.httpdManifest,
'httpdJSPath': self.httpdJSPath,
@ -1190,6 +1204,13 @@ class XPCShellTests(object):
if self.debuggerInfo.interactive:
signal.signal(signal.SIGINT, lambda signum, frame: None)
if self.jsDebuggerInfo:
# The js debugger magic needs more work to do the right thing
# if debugging multiple files.
if len(self.alltests) != 1:
self.log.error("Error: --jsdebugger can only be used with a single test!")
return False
# create a queue of all tests that will run
tests_queue = deque()
# also a list for the tests that need to be run sequentially
@ -1434,6 +1455,13 @@ class XPCShellOptions(OptionParser):
action = "store_true", dest = "debuggerInteractive",
help = "prevents the test harness from redirecting "
"stdout and stderr for interactive debuggers")
self.add_option("--jsdebugger", dest="jsDebugger", action="store_true",
help="Waits for a devtools JS debugger to connect before "
"starting the test.")
self.add_option("--jsdebugger-port", type="int", dest="jsDebuggerPort",
default=6000,
help="The port to listen on for a debugger connection if "
"--jsdebugger is specified.")
def main():
parser = XPCShellOptions()

View File

@ -602,6 +602,9 @@ function ThreadActor(aParent, aGlobal)
this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
this.onNewScript = this.onNewScript.bind(this);
// Set a wrappedJSObject property so |this| can be sent via the observer svc
// for the xpcshell harness.
this.wrappedJSObject = this;
}
ThreadActor.prototype = {
@ -1178,6 +1181,11 @@ ThreadActor.prototype = {
let packet = this._resumed();
this._popThreadPause();
// Tell anyone who cares of the resume (as of now, that's the xpcshell
// harness)
if (Services.obs) {
Services.obs.notifyObservers(this, "devtools-thread-resumed", null);
}
return packet;
}, error => {
return error instanceof Error
@ -1322,7 +1330,7 @@ ThreadActor.prototype = {
for (let line = 0, n = offsets.length; line < n; line++) {
if (offsets[line]) {
let location = { line: line };
let resp = sourceActor._createAndStoreBreakpoint(location);
let resp = sourceActor.createAndStoreBreakpoint(location);
dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
if (resp.error) {
reportError(new Error("Unable to set breakpoint on event listener"));
@ -2516,11 +2524,10 @@ SourceActor.prototype = {
let sourceFetched = fetch(this.url, { loadFromCache: !this.source });
// Record the contentType we just learned during fetching
sourceFetched.then(({ contentType }) => {
this._contentType = contentType;
return sourceFetched.then(result => {
this._contentType = result.contentType;
return result;
});
return sourceFetched;
}
});
},
@ -2848,7 +2855,7 @@ SourceActor.prototype = {
_createBreakpoint: function(loc, originalLoc, condition) {
return resolve(null).then(() => {
return this._createAndStoreBreakpoint({
return this.createAndStoreBreakpoint({
line: loc.line,
column: loc.column,
condition: condition
@ -2915,12 +2922,14 @@ SourceActor.prototype = {
* Create a breakpoint at the specified location and store it in the
* cache. Takes ownership of `aRequest`. This is the
* generated location if this source is sourcemapped.
* Used by the XPCShell test harness to set breakpoints in a script before
* it has loaded.
*
* @param Object aRequest
* An object of the form { line[, column, condition] }. The
* location is in the generated source, if sourcemapped.
*/
_createAndStoreBreakpoint: function (aRequest) {
createAndStoreBreakpoint: function (aRequest) {
let bp = update({}, aRequest, { source: this.form() });
this.breakpointStore.addBreakpoint(bp);
return this._setBreakpoint(aRequest);

View File

@ -13,10 +13,19 @@ const { Cc, Ci } = require("chrome");
Object.defineProperty(this, "addonManager", {
get: (function () {
let cached;
return () => cached
? cached
: (cached = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.amIAddonManager))
return () => {
if (cached === undefined) {
// catch errors as the addonManager might not exist in this environment
// (eg, xpcshell)
try {
cached = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.amIAddonManager);
} catch (ex) {
cached = null;
}
}
return cached;
}
}())
});