// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.sync.ui;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.CustomTabsIntent;
import android.support.v7.app.AlertDialog;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.BuildInfo;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.sync.ProfileSyncService;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.components.sync.PassphraseType;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;
/**
* Dialog to ask to user to enter their sync passphrase.
*/
public class PassphraseDialogFragment extends DialogFragment implements OnClickListener {
private static final String TAG = "Sync_UI";
/**
* A listener for passphrase events.
*/
interface Listener {
/**
* @return whether passphrase was valid.
*/
boolean onPassphraseEntered(String passphrase);
void onPassphraseCanceled();
}
private static final int PASSPHRASE_DIALOG_OK = 0;
private static final int PASSPHRASE_DIALOG_ERROR = 1;
private static final int PASSPHRASE_DIALOG_CANCEL = 2;
private static final int PASSPHRASE_DIALOG_RESET_LINK = 3;
private static final int PASSPHRASE_DIALOG_LIMIT = 4;
private EditText mPassphraseEditText;
private TextView mVerifyingTextView;
private Drawable mOriginalBackground;
private Drawable mErrorBackground;
/**
* Create a new instanceof of {@link PassphraseDialogFragment} and set its arguments.
*/
public static PassphraseDialogFragment newInstance(Fragment target) {
assert ProfileSyncService.get() != null;
PassphraseDialogFragment dialog = new PassphraseDialogFragment();
if (target != null) {
dialog.setTargetFragment(target, -1);
}
return dialog;
}
private void recordPassphraseDialogDismissal(int result) {
RecordHistogram.recordEnumeratedHistogram(
"Sync.PassphraseDialogDismissed",
result,
PASSPHRASE_DIALOG_LIMIT);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
LayoutInflater inflater = getActivity().getLayoutInflater();
View v = inflater.inflate(R.layout.sync_enter_passphrase, null);
TextView promptText = (TextView) v.findViewById(R.id.prompt_text);
promptText.setText(getPromptText());
TextView resetText = (TextView) v.findViewById(R.id.reset_text);
resetText.setText(getResetText());
resetText.setMovementMethod(LinkMovementMethod.getInstance());
resetText.setVisibility(View.VISIBLE);
mVerifyingTextView = (TextView) v.findViewById(R.id.verifying);
mPassphraseEditText = (EditText) v.findViewById(R.id.passphrase);
mPassphraseEditText.setHint(R.string.sync_enter_custom_passphrase_hint);
mPassphraseEditText.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
handleSubmit();
}
return false;
}
});
// Create a new background Drawable for the passphrase EditText to use when the user has
// entered an invalid potential password.
// https://crbug.com/602943 was caused by modifying the Drawable from getBackground()
// without taking a copy.
mOriginalBackground = mPassphraseEditText.getBackground();
mErrorBackground = mOriginalBackground.getConstantState().newDrawable();
mErrorBackground.mutate().setColorFilter(
ApiCompatibilityUtils.getColor(getResources(), R.color.input_underline_error_color),
PorterDuff.Mode.SRC_IN);
final AlertDialog d = new AlertDialog.Builder(getActivity(), R.style.AlertDialogTheme)
.setView(v)
.setPositiveButton(R.string.submit, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface d, int which) {
// We override the onclick. This is a hack to not dismiss the dialog after
// click of OK and instead dismiss it after confirming the passphrase
// is correct.
}
})
.setNegativeButton(R.string.cancel, this)
.setTitle(R.string.sign_in_google_account)
.create();
d.getDelegate().setHandleNativeActionModesEnabled(false);
d.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
Button b = d.getButton(AlertDialog.BUTTON_POSITIVE);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
handleSubmit();
}
});
}
});
return d;
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == AlertDialog.BUTTON_NEGATIVE) {
handleCancel();
}
}
@Override
public void onResume() {
resetPassphraseBoxColor();
super.onResume();
}
private String getPromptText() {
ProfileSyncService pss = ProfileSyncService.get();
String accountName = pss.getCurrentSignedInAccountText() + "\n\n";
PassphraseType passphraseType = pss.getPassphraseType();
if (pss.hasExplicitPassphraseTime()) {
switch (passphraseType) {
case FROZEN_IMPLICIT_PASSPHRASE:
return accountName + pss.getSyncEnterGooglePassphraseBodyWithDateText();
case CUSTOM_PASSPHRASE:
return accountName + pss.getSyncEnterCustomPassphraseBodyWithDateText();
case IMPLICIT_PASSPHRASE: // Falling through intentionally.
case KEYSTORE_PASSPHRASE: // Falling through intentionally.
default:
Log.w(TAG, "Found incorrect passphrase type " + passphraseType
+ ". Falling back to default string.");
return accountName + pss.getSyncEnterCustomPassphraseBodyText();
}
}
return accountName + pss.getSyncEnterCustomPassphraseBodyText();
}
private SpannableString getResetText() {
final Context context = getActivity();
return SpanApplier.applySpans(
context.getString(R.string.sync_passphrase_reset_instructions),
new SpanInfo("<resetlink>", "</resetlink>", new ClickableSpan() {
@Override
public void onClick(View view) {
recordPassphraseDialogDismissal(PASSPHRASE_DIALOG_RESET_LINK);
Uri syncDashboardUrl = Uri.parse(
context.getText(R.string.sync_dashboard_url).toString());
Intent intent = new Intent(Intent.ACTION_VIEW, syncDashboardUrl);
intent.setPackage(BuildInfo.getPackageName(context));
IntentUtils.safePutBinderExtra(
intent, CustomTabsIntent.EXTRA_SESSION, null);
context.startActivity(intent);
}
}));
}
/**
* @return whether the incorrect passphrase text is currently visible.
*/
private boolean isIncorrectPassphraseVisible() {
// Check if the verifying TextView is currently showing the incorrect passphrase text.
String incorrectPassphraseMessage =
getResources().getString(R.string.sync_passphrase_incorrect);
String verifyMessage = mVerifyingTextView.getText().toString();
return verifyMessage.equals(incorrectPassphraseMessage);
}
private void handleCancel() {
int cancelReason = isIncorrectPassphraseVisible()
? PASSPHRASE_DIALOG_ERROR
: PASSPHRASE_DIALOG_CANCEL;
recordPassphraseDialogDismissal(cancelReason);
getListener().onPassphraseCanceled();
}
private void handleSubmit() {
resetPassphraseBoxColor();
mVerifyingTextView.setText(R.string.sync_verifying);
String passphrase = mPassphraseEditText.getText().toString();
boolean success = getListener().onPassphraseEntered(passphrase);
if (success) {
recordPassphraseDialogDismissal(PASSPHRASE_DIALOG_OK);
} else {
invalidPassphrase();
}
}
private Listener getListener() {
Fragment target = getTargetFragment();
if (target instanceof Listener) {
return (Listener) target;
}
return (Listener) getActivity();
}
/**
* Notify this fragment that the passphrase the user entered is incorrect.
*/
private void invalidPassphrase() {
mVerifyingTextView.setText(R.string.sync_passphrase_incorrect);
mVerifyingTextView.setTextColor(ApiCompatibilityUtils.getColor(getResources(),
R.color.input_underline_error_color));
mPassphraseEditText.setBackground(mErrorBackground);
}
private void resetPassphraseBoxColor() {
mPassphraseEditText.setBackground(mOriginalBackground);
}
}