Commit 0ea9325e authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

[TouchToFill][Android] Use RecyclerView for credential items

This CL introduces a RecyclerView and prepares the list of credentials
to display more generic items (like the upcoming header element).

Bug: 1012219
Change-Id: I20b8361b2a139e89a493c5292f86b7bcece1c920
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1857127
Commit-Queue: Friedrich [CET] <fhorschig@chromium.org>
Reviewed-by: default avatarBoris Sazonov <bsazonov@chromium.org>
Cr-Commit-Position: refs/heads/master@{#705982}
parent 890605fb
......@@ -13,17 +13,18 @@ android_library("java") {
"//chrome/android:chrome_java",
"//chrome/browser/touch_to_fill/android:public_java",
"//chrome/browser/util/android:java",
"//third_party/android_deps:com_android_support_recyclerview_v7_java",
"//ui/android:ui_java",
]
java_files = [
"java/src/org/chromium/chrome/browser/touch_to_fill/CredentialProperties.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillBridge.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillCoordinator.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillMediator.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillProperties.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillView.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillViewBinder.java",
"java/src/org/chromium/chrome/browser/touch_to_fill/TouchToFillViewHolder.java",
]
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
......
......@@ -8,6 +8,7 @@
android:descendantFocusability="blocksDescendants"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:minHeight="72dp"
android:gravity="center_vertical"
android:orientation="horizontal"
......
......@@ -44,14 +44,14 @@
android:layout_gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.BlackHint1" />
<ListView
android:id="@+id/credential_list"
<android.support.v7.widget.RecyclerView
android:id="@+id/sheet_item_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:minHeight="200dp"
android:layout_marginTop="24dp"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:divider="@null"
android:dividerHeight="8dp"
tools:listitem="@layout/touch_to_fill_credential_item"/>
</LinearLayout>
// Copyright 2019 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.touch_to_fill;
import android.graphics.Bitmap;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.ui.modelutil.PropertyModel;
/**
* Properties for a credential entry in TouchToFill sheet.
*/
class CredentialProperties {
static final int DEFAULT_ITEM_TYPE = 0; // Credential list has only one entry type.
static final PropertyModel.WritableObjectPropertyKey<Bitmap> FAVICON =
new PropertyModel.WritableObjectPropertyKey<>("favicon");
static final PropertyModel.WritableObjectPropertyKey<Credential> CREDENTIAL =
new PropertyModel.WritableObjectPropertyKey<>("credential");
}
......@@ -4,19 +4,20 @@
package org.chromium.chrome.browser.touch_to_fill;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.DEFAULT_ITEM_TYPE;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.FAVICON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CREDENTIAL_LIST;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FAVICON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import androidx.annotation.Px;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.List;
......@@ -45,25 +46,23 @@ class TouchToFillMediator implements TouchToFillProperties.ViewEventListener {
mModel.set(ORIGIN_SECURE, isOriginSecure);
mModel.set(VISIBLE, true);
ModelList credentialList = mModel.get(CREDENTIAL_LIST);
credentialList.clear();
ListModel<ListItem> sheetItems = mModel.get(SHEET_ITEMS);
sheetItems.clear();
for (Credential credential : credentials) {
PropertyModel propertyModel = new PropertyModel.Builder(FAVICON, CREDENTIAL)
.with(FAVICON, null)
.with(CREDENTIAL, credential)
.build();
credentialList.add(new MVCListAdapter.ListItem(DEFAULT_ITEM_TYPE, propertyModel));
PropertyModel propertyModel =
new PropertyModel.Builder(CredentialProperties.ALL_KEYS)
.with(CREDENTIAL, credential)
.with(ON_CLICK_LISTENER, this::onSelectedCredential)
.build();
sheetItems.add(new ListItem(TouchToFillProperties.ItemType.CREDENTIAL, propertyModel));
mDelegate.fetchFavicon(credential.getOriginUrl(), mDesiredFaviconSize,
(bitmap) -> propertyModel.set(FAVICON, bitmap));
}
}
@Override
public void onSelectItemAt(int position) {
ModelList credentialList = mModel.get(CREDENTIAL_LIST);
assert position >= 0 && position < credentialList.size();
private void onSelectedCredential(Credential credential) {
mModel.set(VISIBLE, false);
mDelegate.onCredentialSelected(credentialList.get(position).model.get(CREDENTIAL));
mDelegate.onCredentialSelected(credential);
}
@Override
......
......@@ -4,9 +4,20 @@
package org.chromium.chrome.browser.touch_to_fill;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import android.graphics.Bitmap;
import androidx.annotation.IntDef;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Properties defined here reflect the visible state of the TouchToFill-components.
*/
......@@ -17,36 +28,71 @@ class TouchToFillProperties {
new PropertyModel.WritableObjectPropertyKey<>("formatted_url");
static final PropertyModel.WritableBooleanPropertyKey ORIGIN_SECURE =
new PropertyModel.WritableBooleanPropertyKey("origin_secure");
static final PropertyModel.ReadableObjectPropertyKey<ModelList> CREDENTIAL_LIST =
new PropertyModel.ReadableObjectPropertyKey<>("credential_list");
static final PropertyModel
.ReadableObjectPropertyKey<ListModel<MVCListAdapter.ListItem>> SHEET_ITEMS =
new PropertyModel.ReadableObjectPropertyKey<>("sheet_items");
static final PropertyModel.ReadableObjectPropertyKey<ViewEventListener> VIEW_EVENT_LISTENER =
new PropertyModel.ReadableObjectPropertyKey<>("view_event_listener");
static PropertyModel createDefaultModel(ViewEventListener listener) {
return new PropertyModel
.Builder(
VISIBLE, FORMATTED_URL, ORIGIN_SECURE, CREDENTIAL_LIST, VIEW_EVENT_LISTENER)
.Builder(VISIBLE, FORMATTED_URL, ORIGIN_SECURE, SHEET_ITEMS, VIEW_EVENT_LISTENER)
.with(VISIBLE, false)
.with(ORIGIN_SECURE, false)
.with(CREDENTIAL_LIST, new ModelList())
.with(SHEET_ITEMS, new ListModel<>())
.with(VIEW_EVENT_LISTENER, listener)
.build();
}
/**
* Properties for a credential entry in TouchToFill sheet.
*/
static class CredentialProperties {
static final PropertyModel.WritableObjectPropertyKey<Bitmap> FAVICON =
new PropertyModel.WritableObjectPropertyKey<>("favicon");
static final PropertyModel.ReadableObjectPropertyKey<Credential> CREDENTIAL =
new PropertyModel.ReadableObjectPropertyKey<>("credential");
static final PropertyModel
.ReadableObjectPropertyKey<Callback<Credential>> ON_CLICK_LISTENER =
new PropertyModel.ReadableObjectPropertyKey<>("on_click_listener");
static final PropertyKey[] ALL_KEYS = {FAVICON, CREDENTIAL, ON_CLICK_LISTENER};
private CredentialProperties() {}
}
/**
* This interface is used by the view to communicate events back to the mediator. It abstracts
* from the view by stripping information like parents, id or context.
*/
interface ViewEventListener {
/**
* Called if the user selected an item from the list.
* @param position The position that the user selected.
*/
void onSelectItemAt(int position);
/** Called if the user dismissed the view. */
void onDismissed();
}
@IntDef({ItemType.HEADER, ItemType.CREDENTIAL})
@Retention(RetentionPolicy.SOURCE)
@interface ItemType {
/**
* The header at the top of the touch to fill sheet.
*/
int HEADER = 1;
/**
* A section containing a user's name and password.
*/
int CREDENTIAL = 2;
}
/**
* Returns the sheet item type for a given item.
* @param item An {@link MVCListAdapter.ListItem}.
* @return The {@link ItemType} of the given list item.
*/
static @ItemType int getItemType(MVCListAdapter.ListItem item) {
return item.type;
}
private TouchToFillProperties() {}
}
......@@ -6,12 +6,11 @@ package org.chromium.chrome.browser.touch_to_fill;
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet;
......@@ -27,7 +26,7 @@ import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
class TouchToFillView implements BottomSheet.BottomSheetContent {
private final Context mContext;
private final BottomSheetController mBottomSheetController;
private final ListView mCredentialListView;
private final RecyclerView mSheetItemListView;
private final LinearLayout mContentView;
private TouchToFillProperties.ViewEventListener mEventListener;
......@@ -51,8 +50,10 @@ class TouchToFillView implements BottomSheet.BottomSheetContent {
mBottomSheetController = bottomSheetController;
mContentView = (LinearLayout) LayoutInflater.from(mContext).inflate(
R.layout.touch_to_fill_sheet, null);
mCredentialListView = mContentView.findViewById(R.id.credential_list);
mCredentialListView.setOnItemClickListener(this::onItemSelected);
mSheetItemListView = mContentView.findViewById(R.id.sheet_item_list);
mSheetItemListView.setLayoutManager(new LinearLayoutManager(
mSheetItemListView.getContext(), LinearLayoutManager.VERTICAL, false));
mSheetItemListView.setItemAnimator(null);
}
/**
......@@ -96,20 +97,14 @@ class TouchToFillView implements BottomSheet.BottomSheetContent {
sheetSubtitleText.setText(subtitleText);
}
void setCredentialListAdapter(ListAdapter adapter) {
mCredentialListView.setAdapter(adapter);
void setSheetItemListAdapter(RecyclerView.Adapter adapter) {
mSheetItemListView.setAdapter(adapter);
}
Context getContext() {
return mContext;
}
private void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
assert adapterView == mCredentialListView : "Use this click handler only for credentials!";
assert mEventListener != null;
mEventListener.onSelectItemAt(i);
}
@Override
public void destroy() {
mBottomSheetController.getBottomSheet().removeObserver(mBottomSheetObserver);
......
......@@ -4,54 +4,111 @@
package org.chromium.chrome.browser.touch_to_fill;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CREDENTIAL_LIST;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FAVICON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VIEW_EVENT_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import static org.chromium.chrome.browser.util.UrlUtilities.stripScheme;
import android.content.Context;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ItemType;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.ui.modelutil.ModelListAdapter;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.RecyclerViewAdapter;
import org.chromium.ui.modelutil.SimpleRecyclerViewMcp;
/**
* Provides functions that map {@link TouchToFillProperties} changes in a {@link PropertyModel} to
* the suitable method in {@link TouchToFillView}.
*/
class TouchToFillViewBinder {
/**
* Called whenever a property in the given model changes. It updates the given view accordingly.
* @param model The observed {@link PropertyModel}. Its data need to be reflected in the view.
* @param view The {@link TouchToFillView} to update.
* @param propertyKey The {@link PropertyKey} which changed.
*/
static void bindTouchToFillView(
PropertyModel model, TouchToFillView view, PropertyKey propertyKey) {
if (propertyKey == VIEW_EVENT_LISTENER) {
view.setEventListener(model.get(VIEW_EVENT_LISTENER));
} else if (propertyKey == VISIBLE) {
view.setVisible(model.get(VISIBLE));
} else if (propertyKey == FORMATTED_URL || propertyKey == ORIGIN_SECURE) {
if (model.get(ORIGIN_SECURE)) {
view.setSecureSubtitle(model.get(FORMATTED_URL));
} else {
view.setNonSecureSubtitle(model.get(FORMATTED_URL));
}
} else if (propertyKey == SHEET_ITEMS) {
view.setSheetItemListAdapter(
new RecyclerViewAdapter<>(new SimpleRecyclerViewMcp<>(model.get(SHEET_ITEMS),
TouchToFillProperties::getItemType,
TouchToFillViewBinder::connectPropertyModel),
TouchToFillViewBinder::createViewHolder));
} else {
assert false : "Unhandled update to property:" + propertyKey;
}
}
/**
* Factory used to create a new View inside the ListView inside the TouchToFillView.
* @param parent The parent {@link ViewGroup} of the new item.
* @param itemType The type of View to create.
*/
static View createCredentialView(Context context) {
return LayoutInflater.from(context).inflate(
R.layout.touch_to_fill_credential_item, null, false);
private static TouchToFillViewHolder createViewHolder(
ViewGroup parent, @ItemType int itemType) {
switch (itemType) {
case ItemType.HEADER:
return null;
case ItemType.CREDENTIAL:
return new TouchToFillViewHolder(parent, R.layout.touch_to_fill_credential_item,
TouchToFillViewBinder::bindCredentialView);
}
assert false : "Cannot create view for ItemType: " + itemType;
return null;
}
/**
* This method creates a model change processor for each recycler view item when it is created.
* @param holder A {@link TouchToFillViewHolder} holding the view and view binder for the MCP.
* @param item A {@link MVCListAdapter.ListItem} holding the {@link PropertyModel} for the MCP.
*/
private static void connectPropertyModel(
TouchToFillViewHolder holder, MVCListAdapter.ListItem item) {
holder.setupModelChangeProcessor(item.model);
}
/**
* Called whenever a credential is bound to this view holder. Please note that this method
* might be called on the same list entry repeatedly, so make sure to always set a default for
* unused fields.
* might be called on the same list entry repeatedly, so make sure to always set a default
* for unused fields.
* @param model The model containing the data for the view
* @param view The view to be bound
* @param propertyKey The key of the property to be bound
*/
static void bindCredentialView(PropertyModel model, View view, PropertyKey propertyKey) {
if (propertyKey == CredentialProperties.FAVICON) {
private static void bindCredentialView(
PropertyModel model, ViewGroup view, PropertyKey propertyKey) {
if (propertyKey == FAVICON) {
ImageView imageView = view.findViewById(R.id.favicon);
imageView.setImageBitmap(model.get(CredentialProperties.FAVICON));
} else if (propertyKey == CredentialProperties.CREDENTIAL) {
Credential credential = model.get(CredentialProperties.CREDENTIAL);
imageView.setImageBitmap(model.get(FAVICON));
} else if (propertyKey == ON_CLICK_LISTENER) {
view.setOnClickListener(clickedView -> {
model.get(ON_CLICK_LISTENER).onResult(model.get(CREDENTIAL));
});
} else if (propertyKey == CREDENTIAL) {
Credential credential = model.get(CREDENTIAL);
TextView pslOriginText = view.findViewById(R.id.credential_origin);
String formattedOrigin = stripScheme(credential.getOriginUrl());
......@@ -68,36 +125,7 @@ class TouchToFillViewBinder {
passwordText.setText(credential.getPassword());
passwordText.setTransformationMethod(new PasswordTransformationMethod());
} else {
assert false : "Every possible property update needs to be handled!";
}
}
/**
* Called whenever a property in the given model changes. It updates the given view accordingly.
* @param model The observed {@link PropertyModel}. Its data need to be reflected in the view.
* @param view The {@link TouchToFillView} to update.
* @param propertyKey The {@link PropertyKey} which changed.
*/
static void bindTouchToFillView(
PropertyModel model, TouchToFillView view, PropertyKey propertyKey) {
if (propertyKey == VIEW_EVENT_LISTENER) {
view.setEventListener(model.get(VIEW_EVENT_LISTENER));
} else if (propertyKey == VISIBLE) {
view.setVisible(model.get(VISIBLE));
} else if (propertyKey == FORMATTED_URL || propertyKey == ORIGIN_SECURE) {
if (model.get(ORIGIN_SECURE)) {
view.setSecureSubtitle(model.get(FORMATTED_URL));
} else {
view.setNonSecureSubtitle(model.get(FORMATTED_URL));
}
} else if (propertyKey == CREDENTIAL_LIST) {
ModelListAdapter adapter = new ModelListAdapter(model.get(CREDENTIAL_LIST));
adapter.registerType(CredentialProperties.DEFAULT_ITEM_TYPE,
() -> TouchToFillViewBinder.createCredentialView(view.getContext()),
TouchToFillViewBinder::bindCredentialView);
view.setCredentialListAdapter(adapter);
} else {
assert false : "Every possible property update needs to be handled!";
assert false : "Unhandled update to property:" + propertyKey;
}
}
......
// Copyright 2019 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.touch_to_fill;
import android.support.annotation.LayoutRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor.ViewBinder;
class TouchToFillViewHolder extends RecyclerView.ViewHolder {
final ViewBinder<PropertyModel, ViewGroup, PropertyKey> mViewBinder;
TouchToFillViewHolder(ViewGroup parent, @LayoutRes int layout,
ViewBinder<PropertyModel, ViewGroup, PropertyKey> viewBinder) {
super(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
mViewBinder = viewBinder;
}
/**
* Called whenever an item is bound to this view holder. Please note that this method
* might be called on the same list entry repeatedly, so make sure to always set a default
* for unused fields.
* @param model The {@link PropertyModel} whose data needs to be displayed.
*/
void setupModelChangeProcessor(PropertyModel model) {
PropertyModelChangeProcessor.create(model, (ViewGroup) itemView, mViewBinder, true);
}
}
\ No newline at end of file
......@@ -5,6 +5,8 @@
package org.chromium.chrome.browser.touch_to_fill;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
......@@ -14,7 +16,7 @@ import static org.chromium.content_public.browser.test.util.TestThreadUtils.runO
import android.support.test.espresso.Espresso;
import android.support.test.filters.MediumTest;
import android.widget.ListView;
import android.support.v7.widget.RecyclerView;
import org.junit.Before;
import org.junit.Rule;
......@@ -36,6 +38,7 @@ import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TouchCommon;
import java.util.Arrays;
import java.util.Collections;
/**
* Integration tests for the Touch To Fill component check that the calls to the Touch To Fill API
......@@ -77,14 +80,15 @@ public class TouchToFillIntegrationTest {
@DisabledTest(message = "crbug.com/1012221")
public void testClickingSuggestionsTriggersCallback() {
runOnUiThreadBlocking(() -> {
mTouchToFill.showCredentials(EXAMPLE_URL, true, Arrays.asList(ANA, BOB));
mTouchToFill.showCredentials(EXAMPLE_URL, true, Collections.singletonList(ANA));
});
pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
pollUiThread(() -> getCredentials().getChildAt(1) != null);
TouchCommon.singleClickView(getCredentials().getChildAt(1));
pollUiThread(() -> getCredentials().getChildAt(0) != null);
TouchCommon.singleClickView(getCredentials().getChildAt(0));
waitForEvent(mMockBridge).onCredentialSelected(BOB);
waitForEvent(mMockBridge).onCredentialSelected(ANA);
verify(mMockBridge).fetchFavicon(eq(ANA.getOriginUrl()), anyInt(), any());
verify(mMockBridge, never()).onDismissed();
}
......@@ -102,8 +106,8 @@ public class TouchToFillIntegrationTest {
verify(mMockBridge, never()).onCredentialSelected(any());
}
private ListView getCredentials() {
return mActivityTestRule.getActivity().findViewById(R.id.credential_list);
private RecyclerView getCredentials() {
return mActivityTestRule.getActivity().findViewById(R.id.sheet_item_list);
}
public static <T> T waitForEvent(T mock) {
......
......@@ -8,22 +8,24 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.DEFAULT_ITEM_TYPE;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.FAVICON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CREDENTIAL_LIST;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
import static org.chromium.content_public.browser.test.util.CriteriaHelper.pollUiThread;
import static java.util.Arrays.asList;
import android.support.test.filters.MediumTest;
import android.support.v7.widget.RecyclerView;
import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import org.junit.Before;
......@@ -33,6 +35,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.Callback;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.ScalableTimeout;
import org.chromium.chrome.browser.ChromeActivity;
......@@ -47,14 +50,24 @@ import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.Collections;
/**
* View tests for the Touch To Fill component ensure that model changes are reflected in the sheet.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class TouchToFillViewTest {
private static final Credential ANA = new Credential("Ana", "S3cr3t", "Ana", "", false);
private static final Credential NO_ONE =
new Credential("", "***", "No Username", "http://m.example.xyz/", true);
private static final Credential BOB =
new Credential("Bob", "***", "Bob", "http://mobile.example.xyz", true);
@Mock
private TouchToFillProperties.ViewEventListener mMockListener;
@Mock
private Callback<Credential> mCredentialCallback;
private PropertyModel mModel;
private TouchToFillView mTouchToFillView;
......@@ -62,12 +75,9 @@ public class TouchToFillViewTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
public TouchToFillViewTest() {
MockitoAnnotations.initMocks(this);
}
@Before
public void setUp() throws InterruptedException {
MockitoAnnotations.initMocks(this);
mActivityTestRule.startMainActivityOnBlankPage();
mModel = TouchToFillProperties.createDefaultModel(mMockListener);
TestThreadUtils.runOnUiThreadBlocking(() -> {
......@@ -118,31 +128,28 @@ public class TouchToFillViewTest {
public void testCredentialsChangedByModel() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mTouchToFillView.setVisible(true);
MVCListAdapter.ModelList credentialList = mModel.get(CREDENTIAL_LIST);
credentialList.add(buildCredentialItem("Ana", "S3cr3t", "Ana", "", false));
credentialList.add(
buildCredentialItem("", "***", "No Username", "http://m.example.xyz/", true));
credentialList.add(
buildCredentialItem("Bob", "***", "Bob", "http://mobile.example.xyz", true));
mModel.get(SHEET_ITEMS)
.addAll(asList(buildCredentialItem(ANA), buildCredentialItem(NO_ONE),
buildCredentialItem(BOB)));
});
pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
assertThat(getCredentials().getChildCount(), is(3));
assertThat(getCredentialOriginAt(0).getVisibility(), is(View.GONE));
assertThat(getCredentialNameAt(0).getText(), is("Ana"));
assertThat(getCredentialPasswordAt(0).getText(), is("S3cr3t"));
assertThat(getCredentialNameAt(0).getText(), is(ANA.getFormattedUsername()));
assertThat(getCredentialPasswordAt(0).getText(), is(ANA.getPassword()));
assertThat(getCredentialPasswordAt(0).getTransformationMethod(),
instanceOf(PasswordTransformationMethod.class));
assertThat(getCredentialOriginAt(1).getVisibility(), is(View.VISIBLE));
assertThat(getCredentialOriginAt(1).getText(), is("m.example.xyz"));
assertThat(getCredentialNameAt(1).getText(), is("No Username"));
assertThat(getCredentialPasswordAt(1).getText(), is("***"));
assertThat(getCredentialNameAt(1).getText(), is(NO_ONE.getFormattedUsername()));
assertThat(getCredentialPasswordAt(1).getText(), is(NO_ONE.getPassword()));
assertThat(getCredentialPasswordAt(1).getTransformationMethod(),
instanceOf(PasswordTransformationMethod.class));
assertThat(getCredentialOriginAt(2).getVisibility(), is(View.VISIBLE));
assertThat(getCredentialOriginAt(2).getText(), is("mobile.example.xyz"));
assertThat(getCredentialNameAt(2).getText(), is("Bob"));
assertThat(getCredentialPasswordAt(2).getText(), is("***"));
assertThat(getCredentialNameAt(2).getText(), is(BOB.getFormattedUsername()));
assertThat(getCredentialPasswordAt(2).getText(), is(BOB.getPassword()));
assertThat(getCredentialPasswordAt(2).getTransformationMethod(),
instanceOf(PasswordTransformationMethod.class));
}
......@@ -151,19 +158,16 @@ public class TouchToFillViewTest {
@MediumTest
public void testCredentialsAreClickable() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mModel.get(CREDENTIAL_LIST)
.add(buildCredentialItem("Carl", "G3h3!m", "Carl", "", false));
mModel.get(CREDENTIAL_LIST)
.add(buildCredentialItem("Bob", "***", "Bob", "m.example.xyz", true));
mModel.get(SHEET_ITEMS).addAll(Collections.singletonList(buildCredentialItem(ANA)));
mModel.set(VISIBLE, true);
});
pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
assertNotNull(getCredentials().getChildAt(1));
assertNotNull(getCredentials().getChildAt(0));
TouchCommon.singleClickView(getCredentials().getChildAt(1));
TouchCommon.singleClickView(getCredentials().getChildAt(0));
waitForEvent().onSelectItemAt(1);
waitForEvent(mCredentialCallback).onResult(eq(ANA));
}
@Test
......@@ -189,8 +193,8 @@ public class TouchToFillViewTest {
return getActivity().getBottomSheet().getSheetState();
}
private ListView getCredentials() {
return mTouchToFillView.getContentView().findViewById(R.id.credential_list);
private RecyclerView getCredentials() {
return mTouchToFillView.getContentView().findViewById(R.id.sheet_item_list);
}
private TextView getCredentialNameAt(int index) {
......@@ -205,20 +209,16 @@ public class TouchToFillViewTest {
return getCredentials().getChildAt(index).findViewById(R.id.credential_origin);
}
TouchToFillProperties.ViewEventListener waitForEvent() {
return verify(mMockListener,
public static <T> T waitForEvent(T mock) {
return verify(mock,
timeout(ScalableTimeout.scaleTimeout(CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL)));
}
private static MVCListAdapter.ListItem buildCredentialItem(String username, String password,
String formattedUsername, String originUrl, boolean isPublicSuffixMatch) {
PropertyModel propertyModel =
new PropertyModel.Builder(FAVICON, CREDENTIAL)
.with(FAVICON, null)
.with(CREDENTIAL,
new Credential(username, password, formattedUsername, originUrl,
isPublicSuffixMatch))
.build();
return new MVCListAdapter.ListItem(DEFAULT_ITEM_TYPE, propertyModel);
private MVCListAdapter.ListItem buildCredentialItem(Credential credential) {
return new MVCListAdapter.ListItem(TouchToFillProperties.ItemType.CREDENTIAL,
new PropertyModel.Builder(TouchToFillProperties.CredentialProperties.ALL_KEYS)
.with(CREDENTIAL, credential)
.with(ON_CLICK_LISTENER, mCredentialCallback)
.build());
}
}
......@@ -13,10 +13,12 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.chromium.chrome.browser.touch_to_fill.CredentialProperties.DEFAULT_ITEM_TYPE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CREDENTIAL_LIST;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.CREDENTIAL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.FAVICON;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.FORMATTED_URL;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ORIGIN_SECURE;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VIEW_EVENT_LISTENER;
import static org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.VISIBLE;
......@@ -34,7 +36,9 @@ import org.mockito.MockitoAnnotations;
import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.touch_to_fill.TouchToFillProperties.ItemType;
import org.chromium.chrome.browser.touch_to_fill.data.Credential;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
......@@ -77,7 +81,7 @@ public class TouchToFillControllerTest {
@Test
public void testCreatesValidDefaultModel() {
assertNotNull(mModel.get(CREDENTIAL_LIST));
assertNotNull(mModel.get(SHEET_ITEMS));
assertNotNull(mModel.get(VIEW_EVENT_LISTENER));
assertThat(mModel.get(VISIBLE), is(false));
assertThat(mModel.get(FORMATTED_URL), is(nullValue()));
......@@ -94,18 +98,19 @@ public class TouchToFillControllerTest {
@Test
public void testShowCredentialsSetsCredentialListAndRequestsFavicons() {
mMediator.showCredentials(TEST_URL, true, Arrays.asList(ANA, CARL, BOB));
MVCListAdapter.ModelList credentialList = mModel.get(CREDENTIAL_LIST);
ListModel<MVCListAdapter.ListItem> credentialList = mModel.get(SHEET_ITEMS);
assertThat(credentialList.size(), is(3));
// TODO(https://crbug.com/1013209): Simplify this after adding equals to ModelList.
assertThat(credentialList.get(0).type, is(DEFAULT_ITEM_TYPE));
assertThat(credentialList.get(0).model.get(CredentialProperties.CREDENTIAL), is(ANA));
assertThat(credentialList.get(0).model.get(CredentialProperties.FAVICON), is(nullValue()));
assertThat(credentialList.get(1).type, is(DEFAULT_ITEM_TYPE));
assertThat(credentialList.get(1).model.get(CredentialProperties.CREDENTIAL), is(CARL));
assertThat(credentialList.get(1).model.get(CredentialProperties.FAVICON), is(nullValue()));
assertThat(credentialList.get(2).type, is(DEFAULT_ITEM_TYPE));
assertThat(credentialList.get(2).model.get(CredentialProperties.CREDENTIAL), is(BOB));
assertThat(credentialList.get(2).model.get(CredentialProperties.FAVICON), is(nullValue()));
assertThat(credentialList.size(), is(3));
assertThat(credentialList.get(0).type, is(ItemType.CREDENTIAL));
assertThat(credentialList.get(0).model.get(CREDENTIAL), is(ANA));
assertThat(credentialList.get(0).model.get(FAVICON), is(nullValue()));
assertThat(credentialList.get(1).type, is(TouchToFillProperties.ItemType.CREDENTIAL));
assertThat(credentialList.get(1).model.get(CREDENTIAL), is(CARL));
assertThat(credentialList.get(1).model.get(FAVICON), is(nullValue()));
assertThat(credentialList.get(2).type, is(TouchToFillProperties.ItemType.CREDENTIAL));
assertThat(credentialList.get(2).model.get(CREDENTIAL), is(BOB));
assertThat(credentialList.get(2).model.get(FAVICON), is(nullValue()));
// ANA and CARL both have TEST_URL as their origin URL
verify(mMockDelegate, times(2)).fetchFavicon(eq(TEST_URL), eq(DESIRED_FAVICON_SIZE), any());
......@@ -115,12 +120,12 @@ public class TouchToFillControllerTest {
@Test
public void testFetchFaviconUpdatesModel() {
mMediator.showCredentials(TEST_URL, true, Collections.singletonList(CARL));
MVCListAdapter.ModelList credentialList = mModel.get(CREDENTIAL_LIST);
ListModel<MVCListAdapter.ListItem> credentialList = mModel.get(SHEET_ITEMS);
assertThat(credentialList.size(), is(1));
// TODO(https://crbug.com/1013209): Simplify this after adding equals to ModelList.
assertThat(credentialList.get(0).type, is(DEFAULT_ITEM_TYPE));
assertThat(credentialList.get(0).model.get(CredentialProperties.CREDENTIAL), is(CARL));
assertThat(credentialList.get(0).model.get(CredentialProperties.FAVICON), is(nullValue()));
assertThat(credentialList.get(0).type, is(TouchToFillProperties.ItemType.CREDENTIAL));
assertThat(credentialList.get(0).model.get(CREDENTIAL), is(CARL));
assertThat(credentialList.get(0).model.get(FAVICON), is(nullValue()));
// ANA and CARL both have TEST_URL as their origin URL
verify(mMockDelegate)
......@@ -130,27 +135,27 @@ public class TouchToFillControllerTest {
Bitmap bitmap = Bitmap.createBitmap(
DESIRED_FAVICON_SIZE, DESIRED_FAVICON_SIZE, Bitmap.Config.ARGB_8888);
callback.onResult(bitmap);
assertThat(credentialList.get(0).model.get(CredentialProperties.FAVICON), is(bitmap));
assertThat(credentialList.get(0).model.get(FAVICON), is(bitmap));
}
@Test
public void testClearsCredentialListWhenShowingAgain() {
mMediator.showCredentials(TEST_URL, true, Collections.singletonList(ANA));
MVCListAdapter.ModelList credentialList = mModel.get(CREDENTIAL_LIST);
ListModel<MVCListAdapter.ListItem> credentialList = mModel.get(SHEET_ITEMS);
// TODO(https://crbug.com/1013209): Simplify this after adding equals to ModelList.
assertThat(credentialList.size(), is(1));
assertThat(credentialList.get(0).type, is(DEFAULT_ITEM_TYPE));
assertThat(credentialList.get(0).model.get(CredentialProperties.CREDENTIAL), is(ANA));
assertThat(credentialList.get(0).model.get(CredentialProperties.FAVICON), is(nullValue()));
assertThat(credentialList.get(0).type, is(ItemType.CREDENTIAL));
assertThat(credentialList.get(0).model.get(CREDENTIAL), is(ANA));
assertThat(credentialList.get(0).model.get(FAVICON), is(nullValue()));
// Showing the sheet a second time should replace all changed credentials.
mMediator.showCredentials(TEST_URL, true, Collections.singletonList(BOB));
credentialList = mModel.get(CREDENTIAL_LIST);
credentialList = mModel.get(SHEET_ITEMS);
// TODO(https://crbug.com/1013209): Simplify this after adding equals to ModelList.
assertThat(credentialList.size(), is(1));
assertThat(credentialList.get(0).type, is(DEFAULT_ITEM_TYPE));
assertThat(credentialList.get(0).model.get(CredentialProperties.CREDENTIAL), is(BOB));
assertThat(credentialList.get(0).model.get(CredentialProperties.FAVICON), is(nullValue()));
assertThat(credentialList.get(0).type, is(ItemType.CREDENTIAL));
assertThat(credentialList.get(0).model.get(CREDENTIAL), is(BOB));
assertThat(credentialList.get(0).model.get(FAVICON), is(nullValue()));
}
@Test
......@@ -162,7 +167,10 @@ public class TouchToFillControllerTest {
@Test
public void testCallsCallbackAndHidesOnSelectingItem() {
mMediator.showCredentials(TEST_URL, true, Arrays.asList(ANA, CARL));
mMediator.onSelectItemAt(1);
assertThat(mModel.get(VISIBLE), is(true));
assertNotNull(mModel.get(SHEET_ITEMS).get(1).model.get(ON_CLICK_LISTENER));
mModel.get(SHEET_ITEMS).get(1).model.get(ON_CLICK_LISTENER).onResult(CARL);
verify(mMockDelegate).onCredentialSelected(CARL);
assertThat(mModel.get(VISIBLE), is(false));
}
......
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