Commit ac8e8d3e authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

[Android] Introduce options to password accessory

This CL renders the options provided by the native backend.
To do that, it introduces new |Item.Type|s.

Bug: 853747
Change-Id: I6fcd833a1674d55da4e4e44d9517e3aff35da823
Reviewed-on: https://chromium-review.googlesource.com/1110217
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarVasilii Sukhanov <vasilii@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#569624}
parent d131a76b
......@@ -397,6 +397,7 @@ java_cpp_enum("chrome_android_java_enums_srcjar") {
"//chrome/browser/notifications/notification_channels_provider_android.h",
"//chrome/browser/notifications/notification_platform_bridge_android.cc",
"//chrome/browser/ntp_snippets/ntp_snippets_metrics.h",
"//chrome/browser/password_manager/password_accessory_view_interface.h",
"//chrome/browser/profiles/profile_metrics.h",
"//chrome/browser/translate/android/translate_utils.h",
"//chrome/browser/ui/android/infobars/infobar_android.h",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<View style="@style/Divider"
xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:fillViewport="true"
android:layout_height="48dp"
android:textAppearance="@style/BlackTitle1"
android:layout_width="match_parent"/>
\ No newline at end of file
......@@ -5,15 +5,12 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntDef;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
import android.view.ViewGroup;
import org.chromium.base.Callback;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
......@@ -148,26 +145,64 @@ public class KeyboardAccessoryData {
private final String mCaption;
private final String mContentDescription;
private final boolean mIsPassword;
private final Callback<Item> mItemSelectedCallback;
private final @Nullable Callback<Item> mItemSelectedCallback;
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_LABEL, TYPE_SUGGESTIONS})
@interface Type {}
public final static int TYPE_LABEL = 1;
public final static int TYPE_SUGGESTIONS = 2;
/**
* Creates a new Item of type {@link ItemType#LABEL}. It is not interactive.
* @param caption The text of the displayed item.
* @param contentDescription The description of this item (i.e. used for accessibility).
*/
public static Item createLabel(String caption, String contentDescription) {
return new Item(ItemType.LABEL, caption, contentDescription, false, null);
}
/**
* Creates a new Item of type {@link ItemType#SUGGESTION} if has a callback, otherwise, it
* will be {@link ItemType#NON_INTERACTIVE_SUGGESTION}. It usually is part of a list of
* suggestions and can have a callback that is triggered on selection.
* @param caption The text of the displayed item. Only plain text if |isPassword| is false.
* @param contentDescription The description of this item (i.e. used for accessibility).
* @param isPassword If true, the displayed caption is transformed into stars.
* @param itemSelectedCallback A click on this item will invoke this callback. Optional.
*/
public static Item createSuggestion(String caption, String contentDescription,
boolean isPassword, @Nullable Callback<Item> itemSelectedCallback) {
if (itemSelectedCallback == null) {
return new Item(ItemType.NON_INTERACTIVE_SUGGESTION, caption, contentDescription,
isPassword, null);
}
return new Item(ItemType.SUGGESTION, caption, contentDescription, isPassword,
itemSelectedCallback);
}
/**
* Creates an Item of type {@link ItemType#DIVIDER}. Basically, it's a horizontal line.
*/
public static Item createDivider() {
return new Item(ItemType.DIVIDER, null, null, false, null);
}
/**
* Creates a new Item of type {@link ItemType#OPTION}. They are normally independent items
* that trigger a unique action (e.g. generate a password or navigate to an overview).
* @param caption The text of the displayed option.
* @param contentDescription The description of this option (i.e. used for accessibility).
* @param callback A click on this item will invoke this callback.
*/
public static Item createOption(
String caption, String contentDescription, Callback<Item> callback) {
return new Item(ItemType.OPTION, caption, contentDescription, false, callback);
}
/**
* Creates a new item.
* @param type Type of the item (e.g. non-clickable TYPE_LABEL or clickable
* TYPE_SUGGESTIONS).
* @param caption The text of the displayed item. Only in plain text if |isPassword| is
* false.
* @param type Type of the item (e.g. non-clickable LABEL or clickable SUGGESTION).
* @param caption The text of the displayed item. Only plain text if |isPassword| is false.
* @param contentDescription The description of this item (i.e. used for accessibility).
* @param isPassword If true, the displayed caption is transformed into stars.
* @param itemSelectedCallback If the Item is interactive, a click on it will trigger this.
*/
public Item(@Type int type, String caption, String contentDescription, boolean isPassword,
Callback<Item> itemSelectedCallback) {
private Item(@ItemType int type, String caption, String contentDescription,
boolean isPassword, @Nullable Callback<Item> itemSelectedCallback) {
mType = type;
mCaption = caption;
mContentDescription = contentDescription;
......@@ -177,9 +212,9 @@ public class KeyboardAccessoryData {
/**
* Returns the type of the item.
* @return Returns a {@link Type}.
* @return Returns a {@link ItemType}.
*/
public @Type int getType() {
public @ItemType int getType() {
return mType;
}
......
......@@ -43,15 +43,40 @@ class PasswordAccessoryBridge {
String[] text, String[] description, int[] isPassword, int[] type) {
Item[] items = new Item[text.length];
for (int i = 0; i < text.length; i++) {
items[i] = new Item(type[i], text[i], description[i], isPassword[i] == 1, (item) -> {
assert mNativeView
!= 0 : "Controller has been destroyed but the bridge wasn't cleaned up!";
nativeOnFillingTriggered(mNativeView, item.isPassword(), item.getCaption());
});
switch (type[i]) {
case ItemType.LABEL:
items[i] = Item.createLabel(text[i], description[i]);
continue;
case ItemType.SUGGESTION:
items[i] = Item.createSuggestion(
text[i], description[i], isPassword[i] == 1, (item) -> {
assert mNativeView
!= 0 : "Controller was destroyed but the bridge wasn't!";
nativeOnFillingTriggered(
mNativeView, item.isPassword(), item.getCaption());
});
continue;
case ItemType.NON_INTERACTIVE_SUGGESTION:
items[i] = Item.createSuggestion(
text[i], description[i], isPassword[i] == 1, null);
continue;
case ItemType.DIVIDER:
items[i] = Item.createDivider();
continue;
case ItemType.OPTION:
items[i] = Item.createOption(text[i], description[i], (item) -> {
assert mNativeView != 0 : "Controller was destroyed but the bridge wasn't!";
nativeOnOptionSelected(mNativeView, item.getCaption());
});
continue;
}
assert false : "Cannot create item for type '" + type[i] + "'.";
}
return items;
}
private native void nativeOnFillingTriggered(
long nativePasswordAccessoryViewAndroid, boolean isPassword, String textToFill);
private native void nativeOnOptionSelected(
long nativePasswordAccessoryViewAndroid, String selectedOption);
}
\ No newline at end of file
......@@ -13,7 +13,7 @@ import android.view.View;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Item;
import org.chromium.chrome.browser.autofill.keyboard_accessory.PasswordAccessorySheetViewBinder.TextViewHolder;
import org.chromium.chrome.browser.autofill.keyboard_accessory.PasswordAccessorySheetViewBinder.ItemViewHolder;
import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter;
import org.chromium.chrome.browser.modelutil.SimpleListObservable;
import org.chromium.chrome.browser.modelutil.SimpleRecyclerViewMcp;
......@@ -78,12 +78,12 @@ public class PasswordAccessorySheetCoordinator {
* @param model the {@link SimpleListObservable<Item>} the adapter gets its data from.
* @return Returns a fully initialized and wired adapter to a PasswordAccessorySheetViewBinder.
*/
static RecyclerViewAdapter<TextViewHolder, Void> createAdapter(
static RecyclerViewAdapter<ItemViewHolder, Void> createAdapter(
SimpleListObservable<Item> model) {
SimpleRecyclerViewMcp<Item, TextViewHolder, Void> processor =
new SimpleRecyclerViewMcp<>(model, Item::getType, TextViewHolder::bind);
RecyclerViewAdapter<TextViewHolder, Void> adapter =
new RecyclerViewAdapter<>(processor, TextViewHolder::create);
SimpleRecyclerViewMcp<Item, ItemViewHolder, Void> processor =
new SimpleRecyclerViewMcp<>(model, Item::getType, ItemViewHolder::bind);
RecyclerViewAdapter<ItemViewHolder, Void> adapter =
new RecyclerViewAdapter<>(processor, ItemViewHolder::create);
model.addObserver(processor);
processor.addObserver(adapter);
return adapter;
......
......@@ -24,45 +24,79 @@ import org.chromium.chrome.browser.modelutil.SimpleListObservable;
*/
class PasswordAccessorySheetViewBinder {
/**
* Holds a TextView that represents a list entry.
* Holds any View that represents a list entry.
*/
static class TextViewHolder extends RecyclerView.ViewHolder {
TextViewHolder(View itemView) {
static class ItemViewHolder extends RecyclerView.ViewHolder {
ItemViewHolder(View itemView) {
super(itemView);
}
static TextViewHolder create(ViewGroup parent, @Item.Type int viewType) {
static ItemViewHolder create(ViewGroup parent, @ItemType int viewType) {
switch (viewType) {
case Item.TYPE_LABEL: {
case ItemType.LABEL:
return new TextViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_accessory_sheet_label, parent,
false));
}
case Item.TYPE_SUGGESTIONS: {
case ItemType.SUGGESTION: // Intentional fallthrough.
case ItemType.NON_INTERACTIVE_SUGGESTION: {
return new TextViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_accessory_sheet_suggestion, parent,
false));
}
case ItemType.DIVIDER:
return new ItemViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_accessory_sheet_divider, parent,
false));
case ItemType.OPTION:
return new TextViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_accessory_sheet_option, parent,
false));
}
assert false : viewType;
return null;
}
void bind(Item item, @Nullable Void payload) {
/**
* Binds the item's state to the held {@link View}. Subclasses of this generic view holder
* might want to actually bind the item state to the view.
* @param item The item that determines the state of the held View.
* @param payload Optional generic payload that might be needed during the binding.
*/
protected void bind(Item item, @Nullable Void payload) {}
}
/**
* Holds a TextView that represents a list entry.
*/
static class TextViewHolder extends ItemViewHolder {
TextViewHolder(View itemView) {
super(itemView);
}
/**
* Returns the text view of this item if there is one.
* @return Returns a {@link TextView}.
*/
private TextView getTextView() {
return (TextView) itemView;
}
@Override
protected void bind(Item item, @Nullable Void payload) {
super.bind(item, payload);
if (item.isPassword()) {
getView().setTransformationMethod(new PasswordTransformationMethod());
getTextView().setTransformationMethod(new PasswordTransformationMethod());
}
getView().setText(item.getCaption());
getTextView().setText(item.getCaption());
if (item.getItemSelectedCallback() != null) {
getView().setOnClickListener(src -> item.getItemSelectedCallback().onResult(item));
getTextView().setOnClickListener(
src -> item.getItemSelectedCallback().onResult(item));
}
}
private TextView getView() {
return (TextView) itemView;
}
}
static void initializeView(RecyclerView view, RecyclerViewAdapter adapter) {
......
......@@ -108,12 +108,11 @@ public class PasswordAccessoryIntegrationTest {
public void testPasswordSheetDisplaysProvidedItems()
throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
provideItems(new Item[] {
createLabel("Passwords"), createSuggestion("mayapark@gmail.com", null),
provideItems(new Item[] {Item.createLabel("Passwords", "Description_Passwords"),
createSuggestion("mayapark@gmail.com", (item) -> {}),
createPassword("SomeHiddenPassword"),
createSuggestion("mayaelisabethmercedesgreenepark@googlemail.com", null),
createPassword("ExtremelyLongPasswordThatUsesQuiteSomeSpaceInTheSheet"),
});
createSuggestion("mayaelisabethmercedesgreenepark@googlemail.com", (item) -> {}),
createPassword("ExtremelyLongPasswordThatUsesQuiteSomeSpaceInTheSheet")});
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
......@@ -127,6 +126,35 @@ public class PasswordAccessoryIntegrationTest {
// TODO(fhorschig): Figure out whether the long name should be cropped or wrapped.
}
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY})
public void testPasswordSheetDisplaysNoPasswordsMessageAndOptions()
throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
final AtomicReference<Item> clicked = new AtomicReference<>();
provideItems(new Item[] {
Item.createLabel("No saved passwords for abc.com", "Description_Passwords"),
Item.createDivider(),
Item.createOption(
"Suggest strong password...", "Description_Generate", clicked::set),
Item.createOption("Manage passwords...", "Description_Manage", (item) -> {})});
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
mHelper.waitForKeyboard();
whenDisplayed(withId(R.id.tabs)).perform(selectTabAtPosition(0));
// Scroll down and click the suggestion.
whenDisplayed(withChild(withText("Suggest strong password...")))
.perform(scrollToPosition(2));
whenDisplayed(withText("Suggest strong password...")).perform(click());
// The callback should have triggered and set the reference to the selected Item.
assertThat(clicked.get(), notNullValue());
assertThat(clicked.get().getCaption(), equalTo("Suggest strong password..."));
}
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY})
......@@ -134,10 +162,11 @@ public class PasswordAccessoryIntegrationTest {
mHelper.loadTestPage(false);
final AtomicReference<Item> clicked = new AtomicReference<>();
provideItems(new Item[] {
createLabel("Passwords"), createSuggestion("mpark@abc.com", null),
createPassword("ShorterPassword"), createSuggestion("mayap@xyz.com", null),
createPassword("PWD"), createSuggestion("park@googlemail.com", null),
createPassword("P@$$W0rt"), createSuggestion("mayapark@gmail.com", clicked::set),
Item.createLabel("Passwords", "Description_Passwords"),
createSuggestion("mpark@abc.com", null), createPassword("ShorterPassword"),
createSuggestion("mayap@xyz.com", null), createPassword("PWD"),
createSuggestion("park@googlemail.com", null), createPassword("P@$$W0rt"),
createSuggestion("mayapark@gmail.com", clicked::set),
createPassword("SomeHiddenLongPassword"),
});
......@@ -189,15 +218,11 @@ public class PasswordAccessoryIntegrationTest {
itemProvider.notifyObservers(items);
}
private static Item createLabel(String caption) {
return new Item(Item.TYPE_LABEL, caption, "Description_" + caption, false, null);
}
private static Item createSuggestion(String caption, Callback<Item> callback) {
return new Item(Item.TYPE_SUGGESTIONS, caption, "Description_" + caption, false, callback);
return Item.createSuggestion(caption, "Description_" + caption, false, callback);
}
private static Item createPassword(String caption) {
return new Item(Item.TYPE_SUGGESTIONS, caption, "Description_" + caption, true, null);
return Item.createSuggestion(caption, "Description_" + caption, true, null);
}
}
......@@ -94,8 +94,7 @@ public class PasswordAccessorySheetViewTest {
public void testAddingCaptionsToTheModelRendersThem() {
assertThat(mView.get().getChildCount(), is(0));
ThreadUtils.runOnUiThreadBlocking(
() -> mModel.add(new Item(Item.TYPE_LABEL, "Passwords", null, false, null)));
ThreadUtils.runOnUiThreadBlocking(() -> mModel.add(Item.createLabel("Passwords", null)));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
......@@ -110,8 +109,8 @@ public class PasswordAccessorySheetViewTest {
ThreadUtils.runOnUiThreadBlocking(
()
-> mModel.add(new Item(Item.TYPE_SUGGESTIONS, "Name Suggestion", null,
false, item -> clicked.set(true))));
-> mModel.add(Item.createSuggestion(
"Name Suggestion", null, false, item -> clicked.set(true))));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
......@@ -131,8 +130,8 @@ public class PasswordAccessorySheetViewTest {
ThreadUtils.runOnUiThreadBlocking(
()
-> mModel.add(new Item(Item.TYPE_SUGGESTIONS, "Password Suggestion", null,
true, item -> clicked.set(true))));
-> mModel.add(Item.createSuggestion(
"Password Suggestion", null, true, item -> clicked.set(true))));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
......
......@@ -13,7 +13,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Item.TYPE_SUGGESTIONS;
import static org.chromium.chrome.browser.tab.Tab.INVALID_TAB_ID;
import static org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType.FROM_BROWSER_ACTIONS;
import static org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType.FROM_CLOSE;
......@@ -150,15 +149,15 @@ public class ManualFillingControllerTest {
Tab firstTab = addTab(mediator, 1111, null);
mController.registerPasswordProvider(firstTabProvider);
firstTabProvider.notifyObservers(new Item[] {
new Item(TYPE_SUGGESTIONS, "FirstPassword", "FirstPassword", true, result -> {})});
Item.createSuggestion("FirstPassword", "FirstPassword", true, result -> {})});
assertThat(mediator.getPasswordAccessorySheet().getModelForTesting().get(0).getCaption(),
is("FirstPassword"));
// Simulate creating a second tab:
Tab secondTab = addTab(mediator, 2222, firstTab);
mController.registerPasswordProvider(secondTabProvider);
secondTabProvider.notifyObservers(new Item[] {new Item(
TYPE_SUGGESTIONS, "SecondPassword", "SecondPassword", true, result -> {})});
secondTabProvider.notifyObservers(new Item[] {
Item.createSuggestion("SecondPassword", "SecondPassword", true, result -> {})});
assertThat(mediator.getPasswordAccessorySheet().getModelForTesting().get(0).getCaption(),
is("SecondPassword"));
......
......@@ -69,8 +69,8 @@ public class PasswordAccessorySheetControllerTest {
public void testModelNotifiesAboutActionsChangedByProvider() {
final KeyboardAccessoryData.PropertyProvider<KeyboardAccessoryData.Item> testProvider =
new KeyboardAccessoryData.PropertyProvider<>();
final KeyboardAccessoryData.Item testItem = new KeyboardAccessoryData.Item(
KeyboardAccessoryData.Item.TYPE_LABEL, "Test Item", null, false, null);
final KeyboardAccessoryData.Item testItem =
KeyboardAccessoryData.Item.createLabel("Test Item", null);
mModel.addObserver(mMockItemListObserver);
mCoordinator.registerItemProvider(testProvider);
......
......@@ -71,6 +71,14 @@ void PasswordAccessoryViewAndroid::OnFillingTriggered(
isPassword, base::android::ConvertJavaStringToUTF16(textToFill));
}
void PasswordAccessoryViewAndroid::OnOptionSelected(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<_jstring*>& selectedOption) {
controller_->OnOptionSelected(
base::android::ConvertJavaStringToUTF16(selectedOption));
}
// static
std::unique_ptr<PasswordAccessoryViewInterface>
PasswordAccessoryViewInterface::Create(
......
......@@ -32,6 +32,10 @@ class PasswordAccessoryViewAndroid : public PasswordAccessoryViewInterface {
const base::android::JavaParamRef<jobject>& obj,
jboolean isPassword,
const base::android::JavaParamRef<jstring>& textToFill);
void OnOptionSelected(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<_jstring*>& selectedOption);
private:
// The controller provides data for this view and owns it.
......
......@@ -71,14 +71,22 @@ void PasswordAccessoryController::OnPasswordsAvailable(
for (const auto& pair : best_matches) {
const PasswordForm* form = pair.second;
base::string16 username = GetDisplayUsername(*form);
items.emplace_back(username, username,
/*is_password=*/false, Item::Type::SUGGESTION);
items.emplace_back(username, username, /*is_password=*/false,
form->username_value.empty()
? Item::Type::NON_INTERACTIVE_SUGGESTION
: Item::Type::SUGGESTION);
items.emplace_back(
form->password_value,
l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_DESCRIPTION, username),
/*is_password=*/true, Item::Type::SUGGESTION);
}
items.emplace_back(base::string16(), base::string16(), false,
Item::Type::DIVIDER);
base::string16 manage_passwords_title = l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK);
items.emplace_back(manage_passwords_title, manage_passwords_title, false,
Item::Type::OPTION);
view_->OnItemsAvailable(origin, items);
}
......
......@@ -71,6 +71,18 @@ MATCHER_P(MatchesLabel, text, PrintItem(text, text, false, ItemType::LABEL)) {
arg.content_description == text && arg.itemType == ItemType::LABEL;
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER(IsDivider, "is a divider") {
return arg.text.empty() && arg.is_password == false &&
arg.content_description.empty() && arg.itemType == ItemType::DIVIDER;
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER_P(MatchesOption, text, PrintItem(text, text, false, ItemType::OPTION)) {
return arg.text == text && arg.is_password == false &&
arg.content_description == text && arg.itemType == ItemType::OPTION;
}
// Compares whether a given AccessoryItem had the given properties.
MATCHER_P4(MatchesItem,
text,
......@@ -121,6 +133,10 @@ base::string16 passwords_title_str(const std::string& domain) {
base::string16 no_user_str() {
return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_EMPTY_LOGIN);
}
base::string16 manage_passwords_str() {
return l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK);
}
} // namespace
......@@ -181,22 +197,25 @@ TEST_F(PasswordAccessoryControllerTest, TransformsMatchesToSuggestions) {
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), true,
ItemType::SUGGESTION))));
ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable({CreateEntry("Ben", "S3cur3").first},
GURL(kExampleSite));
}
TEST_F(PasswordAccessoryControllerTest, HintsToEmptyUserNames) {
EXPECT_CALL(*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(no_user_str(), no_user_str(), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"),
password_for_str(no_user_str()), true,
ItemType::SUGGESTION))));
EXPECT_CALL(
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(no_user_str(), no_user_str(), false,
ItemType::NON_INTERACTIVE_SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"),
password_for_str(no_user_str()), true,
ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable({CreateEntry("", "S3cur3").first},
GURL(kExampleSite));
......@@ -228,7 +247,8 @@ TEST_F(PasswordAccessoryControllerTest, SortsAlphabeticalDuringTransform) {
MatchesItem(ASCIIToUTF16("Zebra"), ASCIIToUTF16("Zebra"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("M3h"), password_for_str("Zebra"), true,
ItemType::SUGGESTION))));
ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable(
{CreateEntry("Ben", "S3cur3").first, CreateEntry("Zebra", "M3h").first,
......@@ -247,7 +267,8 @@ TEST_F(PasswordAccessoryControllerTest, ClearsSuggestionsOnFrameNavigation) {
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)))));
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->DidNavigateMainFrame();
}
......@@ -257,7 +278,8 @@ TEST_F(PasswordAccessoryControllerTest, ProvidesEmptySuggestionsMessage) {
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)))));
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable({}, GURL(kExampleSite));
}
......
......@@ -22,8 +22,26 @@ class PasswordAccessoryViewInterface {
// Represents an item that will be shown in the bottom sheet below a keyboard
// accessory.
struct AccessoryItem {
// Maps to its java counterpart PasswordAccessoryModel.Item.Type.
enum class Type { LABEL = 1, SUGGESTION = 2 };
// Defines which item types exist. A java IntDef@ is generated from this.
// GENERATED_JAVA_ENUM_PACKAGE: (
// org.chromium.chrome.browser.autofill.keyboard_accessory)
// GENERATED_JAVA_CLASS_NAME_OVERRIDE: ItemType
enum class Type {
// An item in title style to purely to display text. Non-interactive.
LABEL = 1, // e.g. the "Passwords for this site" section header.
// An item in list style to displaying an interactive suggestion.
SUGGESTION = 2, // e.g. a user's email address used for sign-up.
// An item in list style to displaying a non-interactive suggestion.
NON_INTERACTIVE_SUGGESTION = 3, // e.g. the "(No username)" suggestion.
// A horizontal, non-interactive divider used to visually divide sections.
DIVIDER = 4,
// A single, usually static and interactive suggestion.
OPTION = 5, // e.g. the "Manage passwords..." link.
};
// The |text| is caption of the item and what will be filled if selected.
base::string16 text;
// The |content_description| is used for accessibility on displayed items.
......
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