Commit 41d06466 authored by Reda Tawfik's avatar Reda Tawfik Committed by Commit Bot

[Android][Mfill] Add search view to AllPasswordsBottomSheet

This CL adds a search view to the bottom sheet and filters
the credentials list with every newly typed character.

Screenshot: https://bugs.chromium.org/p/chromium/issues/detail?id=1104132#c18

Bug: 1104132
Change-Id: Ib9528abcc8ffa3981f37fe3d7334156aa71ff0fb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2391132
Commit-Queue: Reda Tawfik <redatawfik@google.com>
Reviewed-by: default avatarFriedrich [CET] <fhorschig@chromium.org>
Cr-Commit-Position: refs/heads/master@{#807471}
parent 981f8f78
......@@ -3,9 +3,39 @@
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<androidx.recyclerview.widget.RecyclerView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent" />
\ No newline at end of file
android:layout_width="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/sheet_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_passwords_bottom_sheet_title"
android:layout_marginEnd="@dimen/all_passwords_bottom_sheet_padding"
android:layout_marginStart="@dimen/all_passwords_bottom_sheet_padding"
android:layout_marginTop="@dimen/all_passwords_bottom_sheet_padding"
android:textAppearance="@style/TextAppearance.Headline.Primary" />
<SearchView
android:id="@+id/all_passwords_search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:iconifiedByDefault="false"
android:layout_marginEnd="@dimen/all_passwords_bottom_sheet_search_view_padding"
android:layout_marginTop="@dimen/all_passwords_bottom_sheet_search_view_padding"
android:queryHint="@string/all_passwords_bottom_sheet_search_hint">
</SearchView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sheet_item_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginEnd="@dimen/all_passwords_bottom_sheet_recycler_view_padding"
android:layout_marginStart="@dimen/all_passwords_bottom_sheet_recycler_view_padding" />
</LinearLayout>
\ No newline at end of file
......@@ -27,4 +27,7 @@
<dimen name="keyboard_accessory_bar_item_cc_icon_width">32dp</dimen>
<dimen name="keyboard_accessory_tab_icon_width">40dp</dimen>
<dimen name="keyboard_accessory_tab_size">@dimen/keyboard_accessory_height</dimen>
<dimen name="all_passwords_bottom_sheet_padding">20dp</dimen>
<dimen name="all_passwords_bottom_sheet_recycler_view_padding">6dp</dimen>
<dimen name="all_passwords_bottom_sheet_search_view_padding">14dp</dimen>
</resources>
......@@ -44,8 +44,8 @@ class AllPasswordsBottomSheetCoordinator {
*/
public void initialize(Context context, BottomSheetController sheetController,
AllPasswordsBottomSheetCoordinator.Delegate delegate) {
PropertyModel model =
AllPasswordsBottomSheetProperties.createDefaultModel(mMediator::onDismissed);
PropertyModel model = AllPasswordsBottomSheetProperties.createDefaultModel(
mMediator::onDismissed, mMediator::onQueryTextChange);
mMediator.initialize(delegate, model);
setUpModelChangeProcessor(model, new AllPasswordsBottomSheetView(context, sheetController));
}
......
......@@ -11,6 +11,8 @@ import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.Locale;
/**
* Contains the logic for the AllPasswordsBottomSheet. It sets the state of the model and reacts to
* events like clicks.
......@@ -18,6 +20,8 @@ import org.chromium.ui.modelutil.PropertyModel;
class AllPasswordsBottomSheetMediator {
private AllPasswordsBottomSheetCoordinator.Delegate mDelegate;
private PropertyModel mModel;
private Credential[] mCredentials;
private boolean mIsPasswordField;
void initialize(AllPasswordsBottomSheetCoordinator.Delegate delegate, PropertyModel model) {
assert delegate != null;
......@@ -27,11 +31,13 @@ class AllPasswordsBottomSheetMediator {
void showCredentials(Credential[] credentials, boolean isPasswordField) {
assert credentials != null;
mCredentials = credentials;
mIsPasswordField = isPasswordField;
ListModel<ListItem> sheetItems = mModel.get(SHEET_ITEMS);
sheetItems.clear();
for (Credential credential : credentials) {
for (Credential credential : mCredentials) {
final PropertyModel model =
AllPasswordsBottomSheetProperties.CredentialProperties.createCredentialModel(
credential, this::onCredentialSelected, isPasswordField);
......@@ -42,6 +48,42 @@ class AllPasswordsBottomSheetMediator {
mModel.set(VISIBLE, true);
}
/**
* Filters the credentials list based on the passed text and adds the resulting credentials to
* the model.
* @param newText the text used to filter the credentials.
*/
void onQueryTextChange(String newText) {
ListModel<ListItem> sheetItems = mModel.get(SHEET_ITEMS);
sheetItems.clear();
for (Credential credential : mCredentials) {
if (shouldBeFiltered(newText, credential)) continue;
final PropertyModel model =
AllPasswordsBottomSheetProperties.CredentialProperties.createCredentialModel(
credential, this::onCredentialSelected, mIsPasswordField);
sheetItems.add(
new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL, model));
}
}
/**
* Returns true if no substring in the passed credential matches the searchQuery ignoring the
* characters case.
* @param searchQuery the text to check if passed credential has it.
* @param credential its username and origin will be checked for matching string.
* @return Returns whether the entry with the passed credential should be filtered.
*/
private boolean shouldBeFiltered(final String searchQuery, final Credential credential) {
return searchQuery != null
&& !credential.getOriginUrl()
.toLowerCase(Locale.ENGLISH)
.contains(searchQuery.toLowerCase(Locale.ENGLISH))
&& !credential.getUsername()
.toLowerCase(Locale.getDefault())
.contains(searchQuery.toLowerCase(Locale.getDefault()));
}
void onCredentialSelected(Credential credential) {
mModel.set(VISIBLE, false);
mDelegate.onCredentialSelected(credential);
......
......@@ -26,14 +26,19 @@ class AllPasswordsBottomSheetProperties {
new PropertyModel.ReadableObjectPropertyKey<>("dismiss_handler");
static final PropertyModel.ReadableObjectPropertyKey<ListModel<ListItem>> SHEET_ITEMS =
new PropertyModel.ReadableObjectPropertyKey<>("sheet_items");
static final PropertyModel.ReadableObjectPropertyKey<Callback<String>> ON_QUERY_TEXT_CHANGE =
new PropertyModel.ReadableObjectPropertyKey<>("on_query_text_change");
static final PropertyKey[] ALL_KEYS = {VISIBLE, DISMISS_HANDLER, SHEET_ITEMS};
static final PropertyKey[] ALL_KEYS = {
VISIBLE, DISMISS_HANDLER, SHEET_ITEMS, ON_QUERY_TEXT_CHANGE};
static PropertyModel createDefaultModel(Callback<Integer> handler) {
static PropertyModel createDefaultModel(
Callback<Integer> dismissHandler, Callback<String> onSearchQueryChangeHandler) {
return new PropertyModel.Builder(ALL_KEYS)
.with(VISIBLE, false)
.with(SHEET_ITEMS, new ListModel<>())
.with(DISMISS_HANDLER, handler)
.with(DISMISS_HANDLER, dismissHandler)
.with(ON_QUERY_TEXT_CHANGE, onSearchQueryChangeHandler)
.build();
}
......
......@@ -7,6 +7,9 @@ package org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_shee
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
......@@ -28,6 +31,7 @@ class AllPasswordsBottomSheetView implements BottomSheetContent {
private final BottomSheetController mBottomSheetController;
private Callback<Integer> mDismissHandler;
private final RecyclerView mSheetItemListView;
private final LinearLayout mContentView;
private final BottomSheetObserver mBottomSheetObserver = new EmptyBottomSheetObserver() {
@Override
......@@ -57,8 +61,9 @@ class AllPasswordsBottomSheetView implements BottomSheetContent {
public AllPasswordsBottomSheetView(
Context context, BottomSheetController bottomSheetController) {
mBottomSheetController = bottomSheetController;
mSheetItemListView = (RecyclerView) LayoutInflater.from(context).inflate(
mContentView = (LinearLayout) LayoutInflater.from(context).inflate(
R.layout.all_passwords_bottom_sheet, null);
mSheetItemListView = mContentView.findViewById(R.id.sheet_item_list);
mSheetItemListView.setLayoutManager(new LinearLayoutManager(
mSheetItemListView.getContext(), LinearLayoutManager.VERTICAL, false));
mSheetItemListView.setItemAnimator(null);
......@@ -93,9 +98,28 @@ class AllPasswordsBottomSheetView implements BottomSheetContent {
mSheetItemListView.setAdapter(adapter);
}
void setSearchQueryChangeHandler(Callback<String> callback) {
SearchView searchView = getSearchView();
searchView.setOnQueryTextListener(new OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
return false;
}
@Override
public boolean onQueryTextChange(String newString) {
callback.onResult(newString);
return true;
}
});
}
public SearchView getSearchView() {
return mContentView.findViewById(R.id.all_passwords_search_view);
}
@Override
public View getContentView() {
return mSheetItemListView;
return mContentView;
}
@Nullable
......@@ -106,7 +130,7 @@ class AllPasswordsBottomSheetView implements BottomSheetContent {
@Override
public int getVerticalScrollOffset() {
return 0;
return mSheetItemListView.computeVerticalScrollOffset();
}
@Override
......
......@@ -8,6 +8,7 @@ import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_botto
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.CredentialProperties.IS_PASSWORD_FIELD;
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.CredentialProperties.ON_CLICK_LISTENER;
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.DISMISS_HANDLER;
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.ON_QUERY_TEXT_CHANGE;
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.SHEET_ITEMS;
import static org.chromium.chrome.browser.keyboard_accessory.all_passwords_bottom_sheet.AllPasswordsBottomSheetProperties.VISIBLE;
......@@ -50,6 +51,8 @@ class AllPasswordsBottomSheetViewBinder {
view.setDismissHandler(model.get(DISMISS_HANDLER));
} else if (propertyKey == VISIBLE) {
view.setVisible(model.get(VISIBLE));
} else if (propertyKey == ON_QUERY_TEXT_CHANGE) {
view.setSearchQueryChangeHandler(model.get(ON_QUERY_TEXT_CHANGE));
} else if (propertyKey == SHEET_ITEMS) {
view.setSheetItemListAdapter(new RecyclerViewAdapter<>(
new SimpleRecyclerViewMcp<>(model.get(SHEET_ITEMS),
......
......@@ -180,6 +180,12 @@
<message name="IDS_ALL_PASSWORDS_BOTTOM_SHEET_CLOSED" desc="Accessibility string read when the all passwords bottom sheet showing a list of the user's credentials is closed.">
List of credentials to be filled on touch is closed.
</message>
<message name="IDS_ALL_PASSWORDS_BOTTOM_SHEET_TITLE" desc="The title string for the all passwords bottom sheet showing a list of the user's credentials.">
Passwords
</message>
<message name="IDS_ALL_PASSWORDS_BOTTOM_SHEET_SEARCH_HINT" desc="The label for a search button appears in all passwords bottom sheet.">
Search
</message>
<message name="IDS_AUTOFILL_KEYBOARD_ACCESSORY_CONTENT_DESCRIPTION" desc="The text announced by the screen reader when the password suggestions are shown.">
Passwords available
</message>
......
......@@ -61,6 +61,8 @@ public class AllPasswordsBottomSheetViewTest {
private Callback<Integer> mDismissHandler;
@Mock
private Callback<Credential> mCredentialCallback;
@Mock
private Callback<String> mSearchQueryCallback;
private PropertyModel mModel;
private AllPasswordsBottomSheetView mAllPasswordsBottomSheetView;
......@@ -73,7 +75,8 @@ public class AllPasswordsBottomSheetViewTest {
public void setUp() throws InterruptedException {
MockitoAnnotations.initMocks(this);
mActivityTestRule.startMainActivityOnBlankPage();
mModel = AllPasswordsBottomSheetProperties.createDefaultModel(mDismissHandler);
mModel = AllPasswordsBottomSheetProperties.createDefaultModel(
mDismissHandler, mSearchQueryCallback);
mBottomSheetController = mActivityTestRule.getActivity()
.getRootUiCoordinatorForTesting()
.getBottomSheetController();
......@@ -102,24 +105,7 @@ public class AllPasswordsBottomSheetViewTest {
@Test
@MediumTest
public void testCredentialsChangedByModel() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mAllPasswordsBottomSheetView.setVisible(true);
mModel.get(SHEET_ITEMS)
.add(new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
AllPasswordsBottomSheetProperties.CredentialProperties
.createCredentialModel(
ANA, mCredentialCallback, IS_PASSWORD_FIELD)));
mModel.get(SHEET_ITEMS)
.add(new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
AllPasswordsBottomSheetProperties.CredentialProperties
.createCredentialModel(
NO_ONE, mCredentialCallback, IS_PASSWORD_FIELD)));
mModel.get(SHEET_ITEMS)
.add(new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
AllPasswordsBottomSheetProperties.CredentialProperties
.createCredentialModel(
BOB, mCredentialCallback, IS_PASSWORD_FIELD)));
});
addDefaultCredentialsToTheModel();
pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
assertThat(getCredentials().getChildCount(), is(3));
......@@ -163,6 +149,8 @@ public class AllPasswordsBottomSheetViewTest {
@Test
@MediumTest
public void testDismissesWhenHidden() {
addDefaultCredentialsToTheModel();
TestThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, true));
pollUiThread(() -> getBottomSheetState() == SheetState.FULL);
TestThreadUtils.runOnUiThreadBlocking(() -> mModel.set(VISIBLE, false));
......@@ -170,6 +158,35 @@ public class AllPasswordsBottomSheetViewTest {
verify(mDismissHandler).onResult(BottomSheetController.StateChangeReason.NONE);
}
@Test
@MediumTest
public void testSearchIsCalledOnSearchQueryChange() {
addDefaultCredentialsToTheModel();
pollUiThread(() -> mAllPasswordsBottomSheetView.getSearchView().setQuery("a", false));
verify(mSearchQueryCallback).onResult("a");
}
// Adds three credentials items to the model.
private void addDefaultCredentialsToTheModel() {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mAllPasswordsBottomSheetView.setVisible(true);
mModel.get(SHEET_ITEMS)
.add(new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
AllPasswordsBottomSheetProperties.CredentialProperties
.createCredentialModel(
ANA, mCredentialCallback, IS_PASSWORD_FIELD)));
mModel.get(SHEET_ITEMS)
.add(new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
AllPasswordsBottomSheetProperties.CredentialProperties
.createCredentialModel(
NO_ONE, mCredentialCallback, IS_PASSWORD_FIELD)));
mModel.get(SHEET_ITEMS)
.add(new ListItem(AllPasswordsBottomSheetProperties.ItemType.CREDENTIAL,
AllPasswordsBottomSheetProperties.CredentialProperties
.createCredentialModel(
BOB, mCredentialCallback, IS_PASSWORD_FIELD)));
});
}
private ChromeActivity getActivity() {
return mActivityTestRule.getActivity();
}
......@@ -179,7 +196,8 @@ public class AllPasswordsBottomSheetViewTest {
}
private RecyclerView getCredentials() {
return (RecyclerView) mAllPasswordsBottomSheetView.getContentView();
return (RecyclerView) mAllPasswordsBottomSheetView.getContentView().findViewById(
R.id.sheet_item_list);
}
private TextView getCredentialOriginAt(int index) {
......
......@@ -56,7 +56,8 @@ public class AllPasswordsBottomSheetControllerTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
mMediator = new AllPasswordsBottomSheetMediator();
mModel = AllPasswordsBottomSheetProperties.createDefaultModel(mMediator::onDismissed);
mModel = AllPasswordsBottomSheetProperties.createDefaultModel(
mMediator::onDismissed, mMediator::onQueryTextChange);
mMediator.initialize(mMockDelegate, mModel);
}
......@@ -102,4 +103,18 @@ public class AllPasswordsBottomSheetControllerTest {
assertThat(mModel.get(VISIBLE), is(false));
verify(mMockDelegate).onDismissed();
}
@Test
public void testSearchFilterByUsername() {
mMediator.showCredentials(TEST_CREDENTIALS, IS_PASSWORD_FIELD);
mMediator.onQueryTextChange("Bob");
assertThat(mModel.get(SHEET_ITEMS).size(), is(1));
}
@Test
public void testSearchFilterByURL() {
mMediator.showCredentials(TEST_CREDENTIALS, IS_PASSWORD_FIELD);
mMediator.onQueryTextChange("subdomain");
assertThat(mModel.get(SHEET_ITEMS).size(), is(1));
}
}
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