// 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); } }