Commit 96c1d191 authored by Ioana Pandele's avatar Ioana Pandele Committed by Commit Bot

Add explanation text to the password generation dialog

This is the same text that used to be displayed in the password generation
popup. It includes a link sending the user to the place in settings where
they can manage their saved passwords.

Bug: 835234
Change-Id: I7a072fe75323c813b43d20b1fdc545656dff789c
Reviewed-on: https://chromium-review.googlesource.com/1113920Reviewed-by: default avatarMaxim Kolosovskiy <kolos@chromium.org>
Reviewed-by: default avatarTheresa <twellington@chromium.org>
Commit-Queue: Ioana Pandele <ioanap@chromium.org>
Cr-Commit-Position: refs/heads/master@{#571510}
parent 5fa724b4
......@@ -14,5 +14,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/generated_password"
android:textAppearance="@style/BlackHint1"/>
android:textAppearance="@style/BlackHint1"
android:layout_marginBottom="20dp"/>
<org.chromium.ui.widget.TextViewWithClickableSpans
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/generation_save_explanation"
android:textAppearance="@style/BlackBody"
android:layout_marginBottom="20dp"/>
</LinearLayout>
\ No newline at end of file
......@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.password_manager;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.password_manager.PasswordGenerationDialogCoordinator.SaveExplanationText;
import org.chromium.ui.base.WindowAndroid;
/**
......@@ -31,10 +32,13 @@ public class PasswordGenerationDialogBridge {
}
@CalledByNative
public void showDialog(String generatedPassword) {
public void showDialog(String generatedPassword, String explanationString, int linkRangeStart,
int linkRangeEnd) {
mGeneratedPassword = generatedPassword;
mPasswordGenerationDialog.showDialog(
generatedPassword, this::onPasswordAcceptedOrRejected);
mPasswordGenerationDialog.showDialog(generatedPassword,
new SaveExplanationText(explanationString, linkRangeStart, linkRangeEnd,
(view) -> onSavedPasswordsLinkClicked()),
this::onPasswordAcceptedOrRejected);
}
@CalledByNative
......@@ -44,9 +48,7 @@ public class PasswordGenerationDialogBridge {
}
private void onPasswordAcceptedOrRejected(boolean accepted) {
if (mNativePasswordGenerationDialogViewAndroid == 0) {
return;
}
if (mNativePasswordGenerationDialogViewAndroid == 0) return;
if (accepted) {
nativePasswordAccepted(mNativePasswordGenerationDialogViewAndroid, mGeneratedPassword);
......@@ -56,7 +58,14 @@ public class PasswordGenerationDialogBridge {
mPasswordGenerationDialog.dismissDialog();
}
private void onSavedPasswordsLinkClicked() {
if (mNativePasswordGenerationDialogViewAndroid == 0) return;
nativeOnSavedPasswordsLinkClicked(mNativePasswordGenerationDialogViewAndroid);
}
private native void nativePasswordAccepted(
long nativePasswordGenerationDialogViewAndroid, String generatedPassword);
private native void nativePasswordRejected(long nativePasswordGenerationDialogViewAndroid);
private native void nativeOnSavedPasswordsLinkClicked(
long nativePasswordGenerationDialogViewAndroid);
}
......@@ -5,6 +5,7 @@
package org.chromium.chrome.browser.password_manager;
import android.support.annotation.NonNull;
import android.view.View;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.ChromeActivity;
......@@ -19,16 +20,34 @@ public class PasswordGenerationDialogCoordinator {
private final PasswordGenerationDialogModel mModel;
private final PasswordGenerationDialogViewHolder mViewHolder;
/**
* Wrapper class for all the elements needed to display the explanation text.
*/
public static class SaveExplanationText {
public final String mExplanationString;
public final int mLinkRangeStart;
public final int mLinkRangeEnd;
public final Callback<View> mOnLinkClickedCallback;
public SaveExplanationText(String explanationString, int linkRangeStart, int linkRangeEnd,
Callback<View> onLinkClickedCallback) {
mExplanationString = explanationString;
mLinkRangeStart = linkRangeStart;
mLinkRangeEnd = linkRangeEnd;
mOnLinkClickedCallback = onLinkClickedCallback;
}
}
public PasswordGenerationDialogCoordinator(@NonNull ChromeActivity activity) {
mModel = new PasswordGenerationDialogModel();
mViewHolder = new PasswordGenerationDialogViewHolder(activity);
mModalDialogManager = activity.getModalDialogManager();
}
public void showDialog(
String generatedPassword, Callback<Boolean> onPasswordAcceptedOrRejected) {
public void showDialog(String generatedPassword, SaveExplanationText saveExplanationText,
Callback<Boolean> onPasswordAcceptedOrRejected) {
PasswordGenerationDialogMediator.initializeState(
mModel, generatedPassword, onPasswordAcceptedOrRejected);
mModel, generatedPassword, saveExplanationText, onPasswordAcceptedOrRejected);
PasswordGenerationDialogViewBinder.bind(mModel, mViewHolder);
mModalDialogManager.showDialog(mViewHolder.getView(), ModalDialogManager.APP_MODAL);
}
......
......@@ -9,8 +9,10 @@ import org.chromium.base.Callback;
/** Mediator class responsible for initializing the model state. */
public class PasswordGenerationDialogMediator {
public static void initializeState(PasswordGenerationDialogModel model, String password,
PasswordGenerationDialogCoordinator.SaveExplanationText saveExplanationText,
Callback<Boolean> onPasswordAcceptedOrRejected) {
model.setValue(PasswordGenerationDialogModel.GENERATED_PASSWORD, password);
model.setValue(PasswordGenerationDialogModel.SAVE_EXPLANATION_TEXT, saveExplanationText);
model.setValue(PasswordGenerationDialogModel.PASSWORD_ACTION_CALLBACK,
onPasswordAcceptedOrRejected);
}
......
......@@ -15,12 +15,16 @@ class PasswordGenerationDialogModel extends PropertyModel {
/** The generated password to be displayed in the dialog. */
public static final ObjectPropertyKey<String> GENERATED_PASSWORD = new ObjectPropertyKey<>();
/** Explanation text for how the generated password is saved. */
public static final ObjectPropertyKey<PasswordGenerationDialogCoordinator.SaveExplanationText>
SAVE_EXPLANATION_TEXT = new ObjectPropertyKey<>();
/** Callback invoked when the password is accepted or rejected by the user. */
public static final ObjectPropertyKey<Callback<Boolean>> PASSWORD_ACTION_CALLBACK =
new ObjectPropertyKey<>();
/** Default constructor */
public PasswordGenerationDialogModel() {
super(GENERATED_PASSWORD, PASSWORD_ACTION_CALLBACK);
super(GENERATED_PASSWORD, SAVE_EXPLANATION_TEXT, PASSWORD_ACTION_CALLBACK);
}
}
......@@ -49,6 +49,8 @@ public class PasswordGenerationDialogViewBinder {
model.getValue(PasswordGenerationDialogModel.PASSWORD_ACTION_CALLBACK)));
viewHolder.setGeneratedPassword(
model.getValue(PasswordGenerationDialogModel.GENERATED_PASSWORD));
viewHolder.setSaveExplanationText(
model.getValue(PasswordGenerationDialogModel.SAVE_EXPLANATION_TEXT));
viewHolder.initializeView();
}
}
......@@ -6,11 +6,17 @@ package org.chromium.chrome.browser.password_manager;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.widget.TextView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.modaldialog.ModalDialogView;
import org.chromium.chrome.browser.password_manager.PasswordGenerationDialogCoordinator.SaveExplanationText;
import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.widget.TextViewWithClickableSpans;
/** Class holding a {@link ModalDialogView} that is lazily created after all the data is made
* available.
......@@ -18,10 +24,12 @@ import org.chromium.chrome.browser.modaldialog.ModalDialogView;
public class PasswordGenerationDialogViewHolder {
private ModalDialogView mView;
private String mGeneratedPassword;
private SaveExplanationText mSaveExplanationText;
private Context mContext;
private ModalDialogView.Controller mController;
// TODO(ioanap): Change to a text label for now.
private TextView mGeneratedPasswordText;
// TODO(crbug.com/835234): Make the generated password editable.
private TextView mGeneratedPasswordTextView;
private TextViewWithClickableSpans mSaveExplantaionTextView;
public PasswordGenerationDialogViewHolder(Context context) {
mContext = context;
......@@ -31,6 +39,10 @@ public class PasswordGenerationDialogViewHolder {
mGeneratedPassword = generatedPassword;
}
public void setSaveExplanationText(SaveExplanationText saveExplanationText) {
mSaveExplanationText = saveExplanationText;
}
public void setController(ModalDialogView.Controller controller) {
mController = controller;
}
......@@ -42,8 +54,18 @@ public class PasswordGenerationDialogViewHolder {
params.negativeButtonTextId = R.string.password_generation_dialog_cancel_button;
params.customView =
LayoutInflater.from(mContext).inflate(R.layout.password_generation_dialog, null);
mGeneratedPasswordText = params.customView.findViewById(R.id.generated_password);
mGeneratedPasswordText.setText(mGeneratedPassword);
mGeneratedPasswordTextView = params.customView.findViewById(R.id.generated_password);
mGeneratedPasswordTextView.setText(mGeneratedPassword);
mSaveExplantaionTextView = params.customView.findViewById(R.id.generation_save_explanation);
SpannableString explanationSpan =
new SpannableString(mSaveExplanationText.mExplanationString);
explanationSpan.setSpan(
new NoUnderlineClickableSpan(mSaveExplanationText.mOnLinkClickedCallback),
mSaveExplanationText.mLinkRangeStart, mSaveExplanationText.mLinkRangeEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
mSaveExplantaionTextView.setText(explanationSpan);
mSaveExplantaionTextView.setMovementMethod(LinkMovementMethod.getInstance());
mView = new ModalDialogView(mController, params);
}
......
......@@ -1857,6 +1857,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerDisableIncognitoModeUnitTest.java",
"javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerHomepageIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/partnercustomizations/PartnerHomepageUnitTest.java",
"javatests/src/org/chromium/chrome/browser/password_manager/PasswordGenerationDialogTest.java",
"javatests/src/org/chromium/chrome/browser/photo_picker/PhotoPickerDialogTest.java",
"javatests/src/org/chromium/chrome/browser/policy/CombinedPolicyProviderTest.java",
"javatests/src/org/chromium/chrome/browser/payments/AndroidPaymentAppFinderTest.java",
......
// Copyright 2018 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.password_manager;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import android.support.test.filters.SmallTest;
import android.view.View;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordGenerationDialogTest {
private PasswordGenerationDialogCoordinator mDialog;
private String mGeneratedPassword = "generatedpassword";
private String mExplanationString = "This has a link.";
@Mock
private Callback<View> mExplanationTextLinkCallback;
@Mock
private Callback<Boolean> mOnPasswordAcceptedOrRejectedCallback;
@Rule
public ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeActivity.class);
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
@Before
public void setUp() throws InterruptedException {
mActivityTestRule.startMainActivityOnBlankPage();
mDialog = new PasswordGenerationDialogCoordinator(mActivityTestRule.getActivity());
ThreadUtils.runOnUiThreadBlocking(
() -> mDialog.showDialog(mGeneratedPassword,
new PasswordGenerationDialogCoordinator.SaveExplanationText(
mExplanationString, 12, 16, mExplanationTextLinkCallback),
mOnPasswordAcceptedOrRejectedCallback));
}
@Test
@SmallTest
public void testDialogSubviewsData() {
onView(withId(R.id.generated_password)).check(matches(withText(mGeneratedPassword)));
onView(withId(R.id.generation_save_explanation))
.check(matches(withText(mExplanationString)));
}
@Test
@SmallTest
public void testSavedPasswordsLinkCallback() {
onView(withId(R.id.generation_save_explanation)).perform(click());
verify(mExplanationTextLinkCallback).onResult(any());
}
@Test
@SmallTest
public void testAcceptedPasswordCallback() {
onView(withId(R.id.positive_button)).perform(click());
verify(mOnPasswordAcceptedOrRejectedCallback).onResult(true);
}
@Test
@SmallTest
public void testRejectedPasswordCallback() {
onView(withId(R.id.negative_button)).perform(click());
verify(mOnPasswordAcceptedOrRejectedCallback).onResult(false);
}
}
......@@ -6,9 +6,14 @@
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/strings/string16.h"
#include "chrome/browser/password_manager/password_accessory_controller.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "jni/PasswordGenerationDialogBridge_jni.h"
#include "ui/android/window_android.h"
#include "ui/base/l10n/l10n_util.h"
PasswordGenerationDialogViewAndroid::PasswordGenerationDialogViewAndroid(
PasswordAccessoryController* controller)
......@@ -29,9 +34,17 @@ PasswordGenerationDialogViewAndroid::~PasswordGenerationDialogViewAndroid() {
void PasswordGenerationDialogViewAndroid::Show(base::string16& password) {
JNIEnv* env = base::android::AttachCurrentThread();
base::string16 link =
l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_SMART_LOCK);
size_t offset = 0;
base::string16 explanation_text =
l10n_util::GetStringFUTF16(IDS_PASSWORD_GENERATION_PROMPT, link, &offset);
Java_PasswordGenerationDialogBridge_showDialog(
env, java_object_,
base::android::ConvertUTF16ToJavaString(env, password));
env, java_object_, base::android::ConvertUTF16ToJavaString(env, password),
base::android::ConvertUTF16ToJavaString(env, explanation_text), offset,
offset + link.length());
}
void PasswordGenerationDialogViewAndroid::PasswordAccepted(
......@@ -49,6 +62,12 @@ void PasswordGenerationDialogViewAndroid::PasswordRejected(
// mainly for metrics.
}
void PasswordGenerationDialogViewAndroid::OnSavedPasswordsLinkClicked(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj) {
controller_->OnSavedPasswordsLinkClicked();
}
// static
std::unique_ptr<PasswordGenerationDialogViewInterface>
PasswordGenerationDialogViewInterface::Create(
......
......@@ -38,6 +38,11 @@ class PasswordGenerationDialogViewAndroid
void PasswordRejected(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj);
// Called from Java via JNI.
void OnSavedPasswordsLinkClicked(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj);
private:
// The controller provides data for this view and owns it.
PasswordAccessoryController* controller_;
......
......@@ -7,6 +7,7 @@
#include <vector>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/android/preferences/preferences_launcher.h"
#include "chrome/browser/password_manager/password_accessory_view_interface.h"
#include "chrome/browser/password_manager/password_generation_dialog_view_interface.h"
#include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
......@@ -199,6 +200,10 @@ void PasswordAccessoryController::GeneratedPasswordRejected() {
dialog_view_.reset();
}
void PasswordAccessoryController::OnSavedPasswordsLinkClicked() {
chrome::android::PreferencesLauncher::ShowPasswordSettings();
}
gfx::NativeView PasswordAccessoryController::container_view() const {
return web_contents_->GetNativeView();
}
......
......@@ -83,6 +83,10 @@ class PasswordAccessoryController
// Called from the modal dialog if the user rejected the generated password.
void GeneratedPasswordRejected();
// Called from the modal dialog when the user taps on the link contained
// in the explanation text that leads to the saved passwords.
void OnSavedPasswordsLinkClicked();
// The web page view containing the focused field.
gfx::NativeView container_view() const;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment