Commit 7f65a1f2 authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

Introduce password specific accessory sheet view

This CL introduces the view part for the password accessory sheet.
The mediator will follow later with the native part providing the data.

Bug: 811747
Change-Id: I40b04182a369324e5f9fd782b092d352da783fb4
Reviewed-on: https://chromium-review.googlesource.com/1062031
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#559540}
parent 47d67ced
<?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. -->
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@drawable/keyboard_accessory_background"
android:fillViewport="true"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
\ 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:background="@color/google_grey_300"
android:fillViewport="true"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
\ 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:background="@drawable/keyboard_accessory_background"
android:fillViewport="true"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
\ No newline at end of file
...@@ -681,6 +681,13 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -681,6 +681,13 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
return mManualFillingController.getKeyboardAccessory(); return mManualFillingController.getKeyboardAccessory();
} }
/**
* @return The {@link KeyboardAccessoryCoordinator} that belongs to this activity.
*/
public ManualFillingCoordinator getManualFillingController() {
return mManualFillingController;
}
/** /**
* @return The resource id for the menu to use in {@link AppMenu}. Default is R.menu.main_menu. * @return The resource id for the menu to use in {@link AppMenu}. Default is R.menu.main_menu.
*/ */
......
// 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.autofill.keyboard_accessory;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.keyboard_accessory.PasswordAccessorySheetViewBinder.TextViewHolder;
import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter;
import org.chromium.chrome.browser.modelutil.RecyclerViewModelChangeProcessor;
/**
* This component is a tab that can be added to the {@link ManualFillingCoordinator} which shows it
* as bottom sheet below the keyboard accessory.
*/
public class PasswordAccessorySheetCoordinator implements KeyboardAccessoryData.Tab {
private final Context mContext;
private final PasswordAccessorySheetModel mModel = new PasswordAccessorySheetModel();
/**
* Creates the passwords tab.
* @param context The {@link Context} containing resources like icons and layouts for this tab.
*/
public PasswordAccessorySheetCoordinator(Context context) {
mContext = context;
}
@Override
public Drawable getIcon() {
return mContext.getResources().getDrawable(R.drawable.ic_vpn_key_grey);
}
@Override
public String getContentDescription() {
// TODO(fhorschig): Load resource from strings or via mediator from native side.
return null;
}
@Override
public int getTabLayout() {
return R.layout.password_accessory_sheet;
}
@Override
public Listener getListener() {
final PasswordAccessorySheetViewBinder binder = new PasswordAccessorySheetViewBinder();
return view
-> binder.initializeView((RecyclerView) view,
PasswordAccessorySheetCoordinator.createAdapter(mModel, binder));
}
/**
* Creates an adapter to an {@link PasswordAccessorySheetViewBinder} that is wired
* up to the model change processor which listens to the {@link PasswordAccessorySheetModel}.
* @param model the {@link PasswordAccessorySheetModel} the adapter gets its data from.
* @return Returns a fully initialized and wired adapter to a PasswordAccessorySheetViewBinder.
*/
static RecyclerViewAdapter<PasswordAccessorySheetModel, TextViewHolder> createAdapter(
PasswordAccessorySheetModel model, PasswordAccessorySheetViewBinder viewBinder) {
RecyclerViewAdapter<PasswordAccessorySheetModel, TextViewHolder> adapter =
new PasswordAccessorySheetViewAdapter<>(model, viewBinder);
model.addObserver(new RecyclerViewModelChangeProcessor<>(adapter));
return adapter;
}
}
\ No newline at end of file
// 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.autofill.keyboard_accessory;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import org.chromium.chrome.browser.modelutil.ListObservable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
class PasswordAccessorySheetModel extends ListObservable {
static class Item implements KeyboardAccessoryData.Action {
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_LABEL, TYPE_SUGGESTIONS})
@interface Type {}
static final int TYPE_LABEL = 1;
static final int TYPE_SUGGESTIONS = 2;
private final int mViewType;
private final @Nullable Delegate mDelegate;
private final String mCaption;
Item(@Type int viewType, String caption, @Nullable Delegate delegate) {
mViewType = viewType;
mCaption = caption;
mDelegate = delegate;
}
@Type
int getType() {
return this.mViewType;
}
@Override
public String getCaption() {
return this.mCaption;
}
@Override
public Delegate getDelegate() {
return this.mDelegate;
}
}
private final List<Item> mItems = new ArrayList<>();
@Override
public int getItemCount() {
return mItems.size();
}
void addLabel(String caption) {
mItems.add(new Item(Item.TYPE_LABEL, caption, null));
notifyItemRangeInserted(mItems.size() - 1, 1);
}
void addSuggestion(KeyboardAccessoryData.Action action) {
mItems.add(new Item(Item.TYPE_SUGGESTIONS, action.getCaption(), action.getDelegate()));
notifyItemRangeInserted(mItems.size() - 1, 1);
}
public Item getItem(int position) {
return mItems.get(position);
}
}
\ No newline at end of file
// 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.autofill.keyboard_accessory;
class PasswordAccessorySheetView {}
\ No newline at end of file
// 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.autofill.keyboard_accessory;
import android.support.v7.widget.RecyclerView;
import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter;
class PasswordAccessorySheetViewAdapter<VH extends RecyclerView.ViewHolder>
extends RecyclerViewAdapter<PasswordAccessorySheetModel, VH> {
/**
* Construct a new {@link RecyclerViewAdapter}.
*
* @param model The {@link PasswordAccessorySheetModel} model used to retrieve items to
* display in the {@link RecyclerView}.
* @param viewBinder The {@link ViewBinder} binding this adapter to the view holder.
*/
public PasswordAccessorySheetViewAdapter(PasswordAccessorySheetModel model,
ViewBinder<PasswordAccessorySheetModel, VH> viewBinder) {
super(model, viewBinder);
}
@Override
public int getItemViewType(int position) {
return mModel.getItem(position).getType();
}
}
\ No newline at end of file
// 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.autofill.keyboard_accessory;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.autofill.keyboard_accessory.PasswordAccessorySheetModel.Item;
import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter;
/**
* This stateless class provides methods to bind the items in a {@link PasswordAccessorySheetModel}
* to the {@link RecyclerView} used as view of the Password accessory sheet component.
*/
class PasswordAccessorySheetViewBinder
implements RecyclerViewAdapter.ViewBinder<PasswordAccessorySheetModel,
PasswordAccessorySheetViewBinder.TextViewHolder> {
/**
* Holds a TextView that represents a list entry.
*/
static class TextViewHolder extends RecyclerView.ViewHolder {
TextViewHolder(View itemView) {
super(itemView);
}
TextView getView() {
return (TextView) itemView;
}
}
@Override
public TextViewHolder onCreateViewHolder(ViewGroup parent, @Item.Type int viewType) {
if (viewType == Item.TYPE_LABEL) {
return new TextViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_accessory_sheet_label, parent, false));
}
if (viewType == Item.TYPE_SUGGESTIONS) {
return new TextViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.password_accessory_sheet_suggestion, parent, false));
}
assert false : "Every Item.Type needs to be assigned a view!";
return null;
}
@Override
public void onBindViewHolder(
PasswordAccessorySheetModel model, TextViewHolder holder, int position) {
KeyboardAccessoryData.Action item = model.getItem(position);
holder.getView().setText(item.getCaption());
if (item.getDelegate() != null) {
holder.getView().setOnClickListener(src -> item.getDelegate().onActionTriggered(item));
}
}
void initializeView(RecyclerView view, RecyclerViewAdapter adapter) {
view.setLayoutManager(
new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false));
view.setItemAnimator(null);
view.setAdapter(adapter);
}
}
\ No newline at end of file
...@@ -109,6 +109,10 @@ chrome_java_sources = [ ...@@ -109,6 +109,10 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryView.java", "java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryView.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryViewBinder.java", "java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryViewBinder.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/ManualFillingCoordinator.java", "java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/ManualFillingCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetModel.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewBinder.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewAdapter.java",
"java/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTask.java", "java/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTask.java",
"java/src/org/chromium/chrome/browser/banners/AppBannerManager.java", "java/src/org/chromium/chrome/browser/banners/AppBannerManager.java",
"java/src/org/chromium/chrome/browser/banners/AppBannerUiDelegateAndroid.java", "java/src/org/chromium/chrome/browser/banners/AppBannerUiDelegateAndroid.java",
...@@ -1576,6 +1580,7 @@ chrome_test_java_sources = [ ...@@ -1576,6 +1580,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/autofill/PersonalDataManagerTest.java", "javatests/src/org/chromium/chrome/browser/autofill/PersonalDataManagerTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryViewTest.java", "javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/AccessorySheetViewTest.java", "javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/AccessorySheetViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewTest.java",
"javatests/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java", "javatests/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java",
"javatests/src/org/chromium/chrome/browser/banners/InstallerDelegateTest.java", "javatests/src/org/chromium/chrome/browser/banners/InstallerDelegateTest.java",
"javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkBridgeTest.java", "javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkBridgeTest.java",
...@@ -1981,6 +1986,7 @@ chrome_junit_test_java_sources = [ ...@@ -1981,6 +1986,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/AccessorySheetControllerTest.java", "junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/AccessorySheetControllerTest.java",
"junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryControllerTest.java", "junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryControllerTest.java",
"junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/ManualFillingControllerTest.java", "junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/ManualFillingControllerTest.java",
"junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetControllerTest.java",
"junit/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTaskTest.java", "junit/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTaskTest.java",
"junit/src/org/chromium/chrome/browser/browseractions/BrowserActionsIntentTest.java", "junit/src/org/chromium/chrome/browser/browseractions/BrowserActionsIntentTest.java",
"junit/src/org/chromium/chrome/browser/compositor/animation/CompositorAnimatorTest.java", "junit/src/org/chromium/chrome/browser/compositor/animation/CompositorAnimatorTest.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.autofill.keyboard_accessory;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import android.graphics.drawable.Drawable;
import android.support.annotation.LayoutRes;
import android.support.test.filters.MediumTest;
import android.support.v7.widget.RecyclerView;
import android.widget.TextView;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/**
* View tests for the password accessory sheet.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordAccessorySheetViewTest {
private PasswordAccessorySheetModel mModel;
private AtomicReference<RecyclerView> mView = new AtomicReference<>();
@Rule
public ChromeActivityTestRule<ChromeTabbedActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeTabbedActivity.class);
/**
* This helper method inflates the accessory sheet and loads the given layout as minimalistic
* Tab. The passed callback then allows access to the inflated layout.
* @param layout The layout to be inflated.
* @param listener Is called with the inflated layout when the Accessory Sheet initializes it.
*/
private void openLayoutInAccessorySheet(
@LayoutRes int layout, KeyboardAccessoryData.Tab.Listener listener) {
mActivityTestRule.getActivity()
.getManualFillingController()
.getAccessorySheetForTesting()
.addTab(new KeyboardAccessoryData.Tab() {
@Override
public Drawable getIcon() {
return null;
}
@Override
public String getContentDescription() {
return null;
}
@Override
public @LayoutRes int getTabLayout() {
return layout;
}
@Override
public Listener getListener() {
return listener;
}
});
ThreadUtils.runOnUiThreadBlocking(() -> {
mActivityTestRule.getActivity()
.getManualFillingController()
.getAccessorySheetForTesting()
.getMediatorForTesting()
.show();
});
}
@Before
public void setUp() throws InterruptedException {
mModel = new PasswordAccessorySheetModel();
mActivityTestRule.startMainActivityOnBlankPage();
openLayoutInAccessorySheet(R.layout.password_accessory_sheet, view -> {
mView.set((RecyclerView) view);
final PasswordAccessorySheetViewBinder binder = new PasswordAccessorySheetViewBinder();
// Reuse coordinator code to create and wire the adapter. No mediator involved.
binder.initializeView(
mView.get(), PasswordAccessorySheetCoordinator.createAdapter(mModel, binder));
});
CriteriaHelper.pollUiThread(Criteria.equals(true, () -> mView.get() != null));
}
@After
public void tearDown() {
mView.set(null);
}
@Test
@MediumTest
public void testAddingCaptionsToTheModelRendersThem() {
assertThat(mView.get().getChildCount(), is(0));
ThreadUtils.runOnUiThreadBlocking(() -> mModel.addLabel("Passwords"));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
assertThat(((TextView) mView.get().getChildAt(0)).getText(), is("Passwords"));
}
@Test
@MediumTest
public void testAddingSuggestionsToTheModelRendersClickableActions() throws ExecutionException {
final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
final KeyboardAccessoryData.Action testSuggestion = new KeyboardAccessoryData.Action() {
@Override
public String getCaption() {
return "Password Suggestion";
}
@Override
public Delegate getDelegate() {
return action -> clicked.set(true);
}
};
assertThat(mView.get().getChildCount(), is(0));
ThreadUtils.runOnUiThreadBlocking(() -> mModel.addSuggestion(testSuggestion));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
TextView suggestion = (TextView) mView.get().getChildAt(0);
assertThat(suggestion.getText(), is("Password Suggestion"));
ThreadUtils.runOnUiThreadBlocking(suggestion::performClick);
assertThat(clicked.get(), is(true));
}
}
\ No newline at end of file
// 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.autofill.keyboard_accessory;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import android.support.v7.widget.RecyclerView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
/**
* Controller tests for the password accessory sheet.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class PasswordAccessorySheetControllerTest {
@Mock
private RecyclerView mMockView;
private PasswordAccessorySheetCoordinator mCoordinator;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mCoordinator = new PasswordAccessorySheetCoordinator(RuntimeEnvironment.application);
assertNotNull(mCoordinator);
}
@Test
public void testIsValidTab() {
assertNotNull(mCoordinator.getIcon());
assertNotNull(mCoordinator.getListener());
}
@Test
public void testSetsViewAdapterOnTabCreation() {
assertNotNull(mCoordinator.getListener());
mCoordinator.getListener().onTabCreated(mMockView);
verify(mMockView).setAdapter(any());
}
}
\ No newline at end of file
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