Commit 5e1018ba authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

[Android] Manual filling is specific for Browser Tabs

With this CL, the manual filling component ensures that the keyboard
accessory and its sheet display only data that is relevant to the active
tab.

It introduces a mediator controlled and wired up by the already existing
ManualFillingCoordinator. It is responsible to propagate visibility
changes to all subcomponents and update their state depending on the
active browser tab.

Bug: 828832, 811747
Change-Id: I247fcbbc554c06624c12ffe42597d6f671042336
Reviewed-on: https://chromium-review.googlesource.com/1100760
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#568053}
parent 6fbe3983
......@@ -412,10 +412,6 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
getWindowAndroid().setAnimationPlaceholderView(
mCompositorViewHolder.getCompositorView());
mManualFillingController = new ManualFillingCoordinator(getWindowAndroid(),
findViewById(R.id.keyboard_accessory_stub),
findViewById(R.id.keyboard_accessory_sheet_stub));
initializeToolbar();
initializeTabModels();
if (!isFinishing() && getFullscreenManager() != null) {
......@@ -1423,6 +1419,10 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
setStatusBarColor(null, mBaseStatusBarColor);
}, coordinator);
mManualFillingController = new ManualFillingCoordinator(getWindowAndroid(),
findViewById(R.id.keyboard_accessory_stub),
findViewById(R.id.keyboard_accessory_sheet_stub));
if (supportsContextualSuggestionsBottomSheet()
&& FeatureUtilities.isContextualSuggestionsBottomSheetEnabled(isTablet())) {
getLayoutInflater().inflate(R.layout.bottom_sheet, coordinator);
......
......@@ -10,6 +10,7 @@ import android.view.View;
import android.widget.AdapterView;
import android.widget.PopupWindow;
import org.chromium.base.Callback;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.chrome.R;
......@@ -29,7 +30,7 @@ import javax.annotation.Nullable;
@JNINamespace("autofill")
public class PasswordGenerationPopupBridge
implements AdapterView.OnItemClickListener, PopupWindow.OnDismissListener,
PasswordGenerationAdapter.Delegate, Action.Delegate {
PasswordGenerationAdapter.Delegate {
private final long mNativePasswordGenerationPopupViewAndroid;
private final Context mContext;
private final DropdownPopupWindow mPopup;
......@@ -132,7 +133,7 @@ public class PasswordGenerationPopupBridge
// If an action can be shown in the popup, provide the same in the accessory or sheet.
if (providesGenerationAction() && mKeyboardAccessoryAction == null) {
// TODO(ioanap): Move these lines to a new native call or even a separate bridge.
createGeneratePasswordAction(suggestionTitle, this);
createGeneratePasswordAction(suggestionTitle, this::onActionTriggered);
mActionProvider.notifyObservers(new Action[] {mKeyboardAccessoryAction});
// Don't return for now. We want to have both mechanisms in place initially.
}
......@@ -166,8 +167,7 @@ public class PasswordGenerationPopupBridge
if (mPopup != null) mPopup.dismiss();
}
@Override
public void onActionTriggered(Action action) {
private void onActionTriggered(Action action) {
assert action == mKeyboardAccessoryAction;
nativePasswordSelected(mNativePasswordGenerationPopupViewAndroid);
// TODO(ioanap): Create and use this call instead to start the ModalDialog flow:
......@@ -180,18 +180,8 @@ public class PasswordGenerationPopupBridge
|| ChromeFeatureList.isEnabled(ChromeFeatureList.EXPERIMENTAL_UI));
}
private void createGeneratePasswordAction(String caption, Action.Delegate delegate) {
private void createGeneratePasswordAction(String caption, Callback<Action> delegate) {
assert mKeyboardAccessoryAction == null : "Accessory Action should only be created once!";
mKeyboardAccessoryAction = new Action() {
@Override
public String getCaption() {
return caption;
}
@Override
public Delegate getDelegate() {
return delegate;
}
};
mKeyboardAccessoryAction = new Action(caption, delegate);
}
}
......@@ -33,6 +33,12 @@ public class KeyboardAccessoryData {
* @param observer The observer to be notified.
*/
void addObserver(Observer<T> observer);
/**
* Passes the given items to all subscribed {@link Observer}s.
* @param items The array of items to be passed to the {@link Observer}s.
*/
void notifyObservers(T[] items);
}
/**
......@@ -53,11 +59,16 @@ public class KeyboardAccessoryData {
* Describes a tab which should be displayed as a small icon at the start of the keyboard
* accessory. Typically, a tab is responsible to change the bottom sheet below the accessory.
*/
public interface Tab {
public final static class Tab {
private final Drawable mIcon;
private final String mContentDescription;
private final int mTabLayout;
private final @Nullable Listener mListener;
/**
* A Tab's Listener get's notified when e.g. the Tab was assigned a view.
*/
interface Listener {
public interface Listener {
/**
* Triggered when the tab was successfully created.
* @param view The newly created accessory sheet of the tab.
......@@ -65,93 +76,144 @@ public class KeyboardAccessoryData {
void onTabCreated(ViewGroup view);
}
public Tab(Drawable icon, String contentDescription, @LayoutRes int tabLayout,
@Nullable Listener listener) {
mIcon = icon;
mContentDescription = contentDescription;
mTabLayout = tabLayout;
mListener = listener;
}
/**
* Provides the icon that will be displayed in the {@link KeyboardAccessoryCoordinator}.
* @return The small icon that identifies this tab uniquely.
*/
Drawable getIcon();
public Drawable getIcon() {
return mIcon;
}
/**
* The description for this tab. It will become the content description of the icon.
* @return A short string describing the task of this tab.
*/
String getContentDescription();
public String getContentDescription() {
return mContentDescription;
}
/**
* Returns the tab layout which allows to create the tab's view on demand.
* @return The layout resource that allows to create the view necessary for this tab.
*/
@LayoutRes
int getTabLayout();
public @LayoutRes int getTabLayout() {
return mTabLayout;
}
/**
* Returns the listener which might need to react on changes to this tab.
* @return A {@link Listener} to be called, e.g. when the tab is created.
*/
@Nullable
Listener getListener();
public @Nullable Listener getListener() {
return mListener;
}
}
/**
* This describes an action that can be invoked from the keyboard accessory.
* The most prominent example hereof is the "Generate Password" action.
*/
public interface Action {
/**
* The delegate is called when the Action is triggered by a user.
*/
interface Delegate {
/**
* When this function is called, a user interacted with the passed action.
* @param action The action that the user interacted with.
*/
void onActionTriggered(Action action);
public static final class Action {
private final String mCaption;
private final Callback<Action> mActionCallback;
public Action(String caption, Callback<Action> actionCallback) {
mCaption = caption;
mActionCallback = actionCallback;
}
public String getCaption() {
return mCaption;
}
public Callback<Action> getCallback() {
return mActionCallback;
}
String getCaption();
Delegate getDelegate();
}
/**
* This describes an item in a accessory sheet. They are usually list items that were created
* natively.
*/
public interface Item {
public final static class Item {
private final int mType;
private final String mCaption;
private final String mContentDescription;
private final boolean mIsPassword;
private final Callback<Item> mItemSelectedCallback;
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_LABEL, TYPE_SUGGESTIONS})
@interface Type {}
int TYPE_LABEL = 1;
int TYPE_SUGGESTIONS = 2;
public final static int TYPE_LABEL = 1;
public final static int TYPE_SUGGESTIONS = 2;
/**
* 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 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) {
mType = type;
mCaption = caption;
mContentDescription = contentDescription;
mIsPassword = isPassword;
mItemSelectedCallback = itemSelectedCallback;
}
/**
* Returns the type of the item.
* @return Returns a {@link Type}.
*/
@Type
int getType();
public @Type int getType() {
return mType;
}
/**
* Returns a human-readable, translated string that will appear as text of the item.
* @return A short descriptive string of the item.
*/
String getCaption();
public String getCaption() {
return mCaption;
}
/**
* Returns a translated description that can be used for accessibility.
* @return A short description of the displayed item.
*/
String getContentDescription();
public String getContentDescription() {
return mContentDescription;
}
/**
* Returns whether the item (i.e. its caption) contains a password. Can be used to determine
* when to apply text transformations to hide passwords.
* @return Returns true if the caption is a password. False otherwise.
*/
boolean isPassword();
public boolean isPassword() {
return mIsPassword;
}
/**
* The delegate is called when the Item is selected by a user.
*/
Callback<Item> getItemSelectedCallback();
public Callback<Item> getItemSelectedCallback() {
return mItemSelectedCallback;
}
}
/**
......@@ -167,10 +229,7 @@ public class KeyboardAccessoryData {
mObservers.add(observer);
}
/**
* Passes the given items to all subscribed {@link Observer}s.
* @param items The array of items to be passed to the {@link Observer}s.
*/
@Override
public void notifyObservers(T[] items) {
for (Observer<T> observer : mObservers) {
observer.onItemsAvailable(items);
......
......@@ -51,7 +51,7 @@ class KeyboardAccessoryViewBinder
final Action action = actions.get(position);
holder.getActionView().setText(action.getCaption());
holder.getActionView().setOnClickListener(
view -> action.getDelegate().onActionTriggered(action));
view -> action.getCallback().onResult(action));
}
}
......
......@@ -8,6 +8,7 @@ import android.view.ViewStub;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Provider;
import org.chromium.ui.base.WindowAndroid;
/**
......@@ -20,7 +21,6 @@ import org.chromium.ui.base.WindowAndroid;
*/
public class ManualFillingCoordinator {
private final ManualFillingMediator mMediator = new ManualFillingMediator();
// TODO(fhorschig): Manual filling needs a model to map from tab/URL to sheet state/providers.
// Ideally, it just manages |Provider|s and attaches them to the accessory when tabs change.
/**
......@@ -47,14 +47,12 @@ public class ManualFillingCoordinator {
mMediator.destroy();
}
// TODO(fhorschig): Ideally, this isn't exposed. (Apply Hollywood principle to tabs).
void addTab(KeyboardAccessoryData.Tab tab) {
mMediator.addTab(tab);
void registerActionProvider(Provider<KeyboardAccessoryData.Action> actionProvider) {
mMediator.registerActionProvider(actionProvider);
}
// TODO(fhorschig): Ideally, this isn't exposed neither.
void removeTab(KeyboardAccessoryData.Tab tab) {
mMediator.removeTab(tab);
void registerPasswordProvider(Provider<KeyboardAccessoryData.Item> itemProvider) {
mMediator.registerPasswordProvider(itemProvider);
}
// TODO(fhorschig): Should be @VisibleForTesting.
......@@ -67,12 +65,8 @@ public class ManualFillingCoordinator {
return mMediator.getKeyboardAccessory();
}
/**
* Allows access to the accessory sheet.
* @return The coordinator of the Accessory sheet component.
*/
@VisibleForTesting
AccessorySheetCoordinator getAccessorySheetForTesting() {
return mMediator.getAccessorySheet();
ManualFillingMediator getMediatorForTesting() {
return mMediator;
}
}
\ No newline at end of file
......@@ -4,46 +4,143 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.annotation.Nullable;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Provider;
import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
import org.chromium.ui.UiUtils;
import java.util.HashMap;
import java.util.Map;
/**
* This part of the manual filling component manages the state of the manual filling flow depending
* on the currently shown tab.
*/
class ManualFillingMediator implements KeyboardAccessoryCoordinator.VisibilityDelegate {
class ManualFillingMediator
extends EmptyTabObserver implements KeyboardAccessoryCoordinator.VisibilityDelegate {
/**
* Provides a cache for a given Provider which can repeat the last notification to all
* observers.
* @param <T> The data that is sent to observers.
*/
private class ProviderCacheAdapter<T> extends KeyboardAccessoryData.PropertyProvider<T>
implements KeyboardAccessoryData.Observer<T> {
private final Tab mTab;
private T[] mLastItems;
/**
* Creates an adapter that listens to the given |provider| and stores items provided by it.
* If the observed provider serves a currently visible tab, the data is immediately sent on.
* @param tab The {@link Tab} which the given Provider should affect immediately.
* @param provider The {@link Provider} to observe and whose data to cache.
*/
ProviderCacheAdapter(Tab tab, Provider<T> provider) {
mTab = tab;
provider.addObserver(this);
}
void notifyAboutCachedItems() {
notifyObservers(mLastItems);
}
@Override
public void onItemsAvailable(T[] items) {
mLastItems = items;
// Update the contents immediately, if the adapter connects to an active element.
if (mTab == mActiveBrowserTab) notifyObservers(items);
}
}
/**
* This class holds all data that is necessary to restore the state of the Keyboard accessory
* and its sheet for a given tab.
*/
@VisibleForTesting
static class AccessoryState {
@Nullable
ProviderCacheAdapter<KeyboardAccessoryData.Action> mActionsProvider;
@Nullable
PasswordAccessorySheetCoordinator mPasswordAccessorySheet;
}
// TODO(fhorschig): Do we need a MapObservable type? (This would be only observer though).
private final Map<Tab, AccessoryState> mModel = new HashMap<>();
private KeyboardAccessoryCoordinator mKeyboardAccessory;
private AccessorySheetCoordinator mAccessorySheet;
private ChromeActivity mActivity; // Used to control the keyboard.
private TabModelSelectorTabModelObserver mTabModelObserver;
private Tab mActiveBrowserTab;
private final TabObserver mTabObserver = new EmptyTabObserver() {
@Override
public void onHidden(Tab tab) {
// TODO(fhorschig): Test that the accessory is reset and close if a tab changes.
// TODO(fhorschig): Test that this hides everything.
resetAccessory(tab);
}
@Override
public void onDestroyed(Tab tab) {
// TODO(fhorschig): Test that this hides everything.
// TODO(fhorschig): Clear caches and remove tab. What to select next?
}
@Override
public void onEnterFullscreenMode(Tab tab, FullscreenOptions options) {
// TODO(fhorschig): Test that this hides everything.
}
};
void initialize(KeyboardAccessoryCoordinator keyboardAccessory,
AccessorySheetCoordinator accessorySheet, ChromeActivity activity) {
mKeyboardAccessory = keyboardAccessory;
mAccessorySheet = accessorySheet;
mActivity = activity;
mTabModelObserver = new TabModelSelectorTabModelObserver(mActivity.getTabModelSelector()) {
@Override
public void didSelectTab(Tab tab, TabModel.TabSelectionType type, int lastId) {
mActiveBrowserTab = tab;
restoreCachedState(tab);
}
@Override
public void willCloseTab(Tab tab, boolean animate) {
mModel.remove(tab);
}
};
Tab currentTab = activity.getTabModelSelector().getCurrentTab();
if (currentTab != null) {
mTabModelObserver.didSelectTab(
currentTab, TabModel.TabSelectionType.FROM_USER, Tab.INVALID_TAB_ID);
}
}
void destroy() {
// TODO(fhorschig): Remove all tabs. Destroy all providers.
// mPasswordAccessorySheet.destroy();
mKeyboardAccessory.destroy();
void registerPasswordProvider(Provider<KeyboardAccessoryData.Item> itemProvider) {
assert getPasswordAccessorySheet() != null : "No password sheet available!";
getPasswordAccessorySheet().registerItemProvider(itemProvider);
}
/**
* Links a tab to the manual UI by adding it to the held {@link AccessorySheetCoordinator} and
* the {@link KeyboardAccessoryCoordinator}.
* @param tab The tab component to be added.
*/
void addTab(KeyboardAccessoryData.Tab tab) {
// TODO(fhorschig): Should add Tabs per URL/chome tab.
mKeyboardAccessory.addTab(tab);
mAccessorySheet.addTab(tab);
void registerActionProvider(Provider<KeyboardAccessoryData.Action> actionProvider) {
ProviderCacheAdapter<KeyboardAccessoryData.Action> adapter =
new ProviderCacheAdapter<>(mActiveBrowserTab, actionProvider);
mModel.get(mActiveBrowserTab).mActionsProvider = adapter;
getKeyboardAccessory().registerActionListProvider(adapter);
}
void removeTab(KeyboardAccessoryData.Tab tab) {
// TODO(fhorschig): Should remove Tabs per URL/chome tab.
mKeyboardAccessory.removeTab(tab);
mAccessorySheet.removeTab(tab);
void destroy() {
// TODO(fhorschig): Remove all tabs. Remove observers from tabs. Destroy all providers.
mTabModelObserver.destroy();
mKeyboardAccessory.destroy();
}
@Override
......@@ -73,13 +170,78 @@ class ManualFillingMediator implements KeyboardAccessoryCoordinator.VisibilityDe
UiUtils.hideKeyboard(mActivity.getCurrentFocus());
}
// TODO(fhorschig): Should be @VisibleForTesting.
AccessorySheetCoordinator getAccessorySheet() {
return mAccessorySheet;
private AccessoryState getOrCreateAccessoryState(Tab tab) {
AccessoryState state = mModel.get(tab);
if (state != null) return state;
state = new AccessoryState();
mModel.put(tab, state);
tab.addObserver(mTabObserver);
return state;
}
private void restoreCachedState(Tab browserTab) {
AccessoryState state = getOrCreateAccessoryState(browserTab);
if (state.mPasswordAccessorySheet != null) {
addTab(state.mPasswordAccessorySheet.getTab());
}
if (state.mActionsProvider != null) state.mActionsProvider.notifyAboutCachedItems();
}
private void resetAccessory(Tab browserTab) {
AccessoryState state = getOrCreateAccessoryState(browserTab);
if (state.mPasswordAccessorySheet != null) {
removeTab(state.mPasswordAccessorySheet.getTab());
}
}
private void removeTab(KeyboardAccessoryData.Tab tab) {
mKeyboardAccessory.removeTab(tab);
mAccessorySheet.removeTab(tab);
}
@VisibleForTesting
void addTab(KeyboardAccessoryData.Tab tab) {
mKeyboardAccessory.addTab(tab);
mAccessorySheet.addTab(tab);
}
@VisibleForTesting
@Nullable
PasswordAccessorySheetCoordinator getPasswordAccessorySheet() {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.EXPERIMENTAL_UI)
&& !ChromeFeatureList.isEnabled(ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY)) {
return null;
}
AccessoryState state = getOrCreateAccessoryState(mActiveBrowserTab);
if (state.mPasswordAccessorySheet == null) {
state.mPasswordAccessorySheet = new PasswordAccessorySheetCoordinator(mActivity);
addTab(state.mPasswordAccessorySheet.getTab());
}
return state.mPasswordAccessorySheet;
}
// TODO(fhorschig): Should be @VisibleForTesting.
KeyboardAccessoryCoordinator getKeyboardAccessory() {
return mKeyboardAccessory;
}
@VisibleForTesting
AccessorySheetCoordinator getAccessorySheet() {
return mAccessorySheet;
}
@VisibleForTesting
TabModelObserver getTabModelObserverForTesting() {
return mTabModelObserver;
}
@VisibleForTesting
TabObserver getTabObserverForTesting() {
return mTabObserver;
}
@VisibleForTesting
Map<Tab, AccessoryState> getModelForTesting() {
return mModel;
}
}
......@@ -4,7 +4,6 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import org.chromium.base.Callback;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Item;
......@@ -13,7 +12,6 @@ import org.chromium.ui.base.WindowAndroid;
class PasswordAccessoryBridge {
private final KeyboardAccessoryData.PropertyProvider<Item> mItemProvider =
new KeyboardAccessoryData.PropertyProvider<>();
private final KeyboardAccessoryData.Tab mTab;
private final ManualFillingCoordinator mManualFillingCoordinator;
private long mNativeView;
......@@ -21,14 +19,7 @@ class PasswordAccessoryBridge {
mNativeView = nativeView;
ChromeActivity activity = (ChromeActivity) windowAndroid.getActivity().get();
mManualFillingCoordinator = activity.getManualFillingController();
// TODO(fhorschig): This belongs into the ManualFillingCoordinator.
PasswordAccessorySheetCoordinator passwordAccessorySheet =
new PasswordAccessorySheetCoordinator(activity);
mTab = passwordAccessorySheet.createTab();
// TODO(fhorschig): This is not correct - the passwords need to be mapped to a tab/URL. The
// provider can be registered to the ManualFillingCoordinator - not directly to the sheet.
passwordAccessorySheet.registerItemProvider(mItemProvider);
mManualFillingCoordinator.addTab(mTab);
mManualFillingCoordinator.registerPasswordProvider(mItemProvider);
}
@CalledByNative
......@@ -44,49 +35,19 @@ class PasswordAccessoryBridge {
@CalledByNative
private void destroy() {
mItemProvider.notifyObservers(new Item[] {}); // There are no more items available!
mNativeView = 0;
mManualFillingCoordinator.removeTab(mTab); // TODO(fhorschig): Should be "unregister".
}
private Item[] convertToItems(
String[] text, String[] description, int[] isPassword, int[] itemType) {
String[] text, String[] description, int[] isPassword, int[] type) {
Item[] items = new Item[text.length];
for (int i = 0; i < text.length; i++) {
final String textToFill = text[i];
final String contentDescription = description[i];
final int type = itemType[i];
final boolean password = isPassword[i] == 1;
items[i] = new Item() {
@Override
public int getType() {
return type;
}
@Override
public String getCaption() {
return textToFill;
}
@Override
public String getContentDescription() {
return contentDescription;
}
@Override
public boolean isPassword() {
return password;
}
@Override
public Callback<Item> getItemSelectedCallback() {
return (item) -> {
assert mNativeView
!= 0
: "Controller has been destroyed but the bridge wasn't cleaned up!";
nativeOnFillingTriggered(mNativeView, item.getCaption());
};
}
};
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.getCaption());
});
}
return items;
}
......
......@@ -6,7 +6,9 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
......@@ -24,6 +26,37 @@ public class PasswordAccessorySheetCoordinator {
private final Context mContext;
private final SimpleListObservable<Item> mModel = new SimpleListObservable<>();
private final KeyboardAccessoryData.Observer<Item> mMediator = mModel::set;
private final PasswordAccessorySheetViewBinder mBinder = new PasswordAccessorySheetViewBinder();
private final KeyboardAccessoryData.Tab mTab;
@VisibleForTesting
static class IconProvider {
private final static IconProvider sInstance = new IconProvider();
private Drawable mIcon;
private IconProvider() {}
public static IconProvider getInstance() {
return sInstance;
}
/**
* Loads and remembers the icon used for this class. Used to mock icons in unit tests.
* @param context The context containing the icon resources.
* @return The icon as {@link Drawable}.
*/
public Drawable getIcon(Context context) {
if (mIcon != null) return mIcon;
mIcon = VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_vpn_key_grey, context.getTheme());
return mIcon;
}
@VisibleForTesting
void setIconForTesting(Drawable icon) {
mIcon = icon;
}
}
/**
* Creates the passwords tab.
......@@ -31,6 +64,14 @@ public class PasswordAccessorySheetCoordinator {
*/
public PasswordAccessorySheetCoordinator(Context context) {
mContext = context;
mTab = new KeyboardAccessoryData.Tab(IconProvider.getInstance().getIcon(mContext),
null, // TODO(fhorschig): Load from strings or native side.
R.layout.password_accessory_sheet, this::onTabCreated);
}
private void onTabCreated(View view) {
mBinder.initializeView((RecyclerView) view,
PasswordAccessorySheetCoordinator.createAdapter(mModel, mBinder));
}
/**
......@@ -47,6 +88,7 @@ public class PasswordAccessorySheetCoordinator {
return adapter;
}
// TODO(fhorschig): There is only one. Make this a ctor param and self-destruct with it.
/**
* Registered item providers can replace the currently shown data in the password sheet.
* @param actionProvider The provider this component will listen to.
......@@ -56,37 +98,12 @@ public class PasswordAccessorySheetCoordinator {
}
/**
* Returns a new Tab object that describes the appearance of this class in the keyboard
* keyboard accessory or its accessory sheet.
* @return Returns a new {@link KeyboardAccessoryData.Tab} that is connected to this sheet.
* Returns the Tab object that describes the appearance of this class in the keyboard accessory
* or its accessory sheet. The returned object doesn't change for this instance.
* @return Returns a stable {@link KeyboardAccessoryData.Tab} that is connected to this sheet.
*/
public KeyboardAccessoryData.Tab createTab() {
return new KeyboardAccessoryData.Tab() {
@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));
}
};
public KeyboardAccessoryData.Tab getTab() {
return mTab;
}
@VisibleForTesting
......
......@@ -1629,10 +1629,11 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/autofill/AutofillTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/AutofillTestHelper.java",
"javatests/src/org/chromium/chrome/browser/autofill/PersonalDataManagerTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/AccessorySheetViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryViewTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/ManualFillingIntegrationTest.java",
"javatests/src/org/chromium/chrome/browser/autofill/keyboard_accessory/ManualFillingTestHelper.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/PasswordAccessoryIntegrationTest.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/InstallerDelegateTest.java",
......
......@@ -18,8 +18,6 @@ import static org.junit.Assert.assertTrue;
import static org.chromium.chrome.test.util.ViewUtils.waitForView;
import android.graphics.drawable.Drawable;
import android.support.annotation.LayoutRes;
import android.support.test.filters.MediumTest;
import android.support.v4.view.ViewPager;
import android.view.View;
......@@ -37,6 +35,7 @@ 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.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Tab;
import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter;
import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor;
import org.chromium.chrome.test.ChromeActivityTestRule;
......@@ -96,7 +95,7 @@ public class AccessorySheetViewTest {
@MediumTest
public void testAddingTabToModelRendersTabsView() {
final String kSampleAction = "Some Action";
mModel.getTabList().add(createTestTab(view -> {
mModel.getTabList().add(new Tab(null, null, R.layout.empty_accessory_sheet, view -> {
assertNotNull("The tab must have been created!", view);
assertTrue("Empty tab is a layout.", view instanceof LinearLayout);
LinearLayout baseLayout = (LinearLayout) view;
......@@ -173,32 +172,8 @@ public class AccessorySheetViewTest {
onView(isRoot()).check((r, e) -> waitForView((ViewGroup) r, withText(kSecondTab)));
}
private KeyboardAccessoryData.Tab createTestTab(KeyboardAccessoryData.Tab.Listener listener) {
return new KeyboardAccessoryData.Tab() {
@Override
public Drawable getIcon() {
return null; // Unused.
}
@Override
public String getContentDescription() {
return null; // Unused.
}
@Override
public @LayoutRes int getTabLayout() {
return R.layout.empty_accessory_sheet;
}
@Override
public Listener getListener() {
return listener;
}
};
}
private KeyboardAccessoryData.Tab createTestTabWithTextView(String textViewCaption) {
return createTestTab(view -> {
private Tab createTestTabWithTextView(String textViewCaption) {
return new Tab(null, null, R.layout.empty_accessory_sheet, view -> {
TextView sampleTextView = new TextView(mActivityTestRule.getActivity());
sampleTextView.setText(textViewCaption);
view.addView(sampleTextView);
......
......@@ -25,7 +25,6 @@ import static org.chromium.chrome.test.util.ViewUtils.VIEW_INVISIBLE;
import static org.chromium.chrome.test.util.ViewUtils.VIEW_NULL;
import static org.chromium.chrome.test.util.ViewUtils.waitForView;
import android.graphics.drawable.Drawable;
import android.support.test.filters.MediumTest;
import android.support.v7.widget.AppCompatImageView;
import android.view.View;
......@@ -63,44 +62,13 @@ public class KeyboardAccessoryViewTest {
public ChromeActivityTestRule<ChromeTabbedActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeTabbedActivity.class);
private static KeyboardAccessoryData.Action createTestAction(
String caption, KeyboardAccessoryData.Action.Delegate delegate) {
return new KeyboardAccessoryData.Action() {
@Override
public String getCaption() {
return caption;
}
@Override
public Delegate getDelegate() {
return delegate;
}
};
}
private KeyboardAccessoryData.Tab createTestTab(String contentDescription) {
return new KeyboardAccessoryData.Tab() {
@Override
public Drawable getIcon() {
return mActivityTestRule.getActivity().getResources().getDrawable(
android.R.drawable.ic_lock_lock);
}
@Override
public String getContentDescription() {
return contentDescription;
}
@Override
public int getTabLayout() {
return R.layout.empty_accessory_sheet; // Unused.
}
@Override
public Listener getListener() {
return null; // Unused.
}
};
return new KeyboardAccessoryData.Tab(
mActivityTestRule.getActivity().getResources().getDrawable(
android.R.drawable.ic_lock_lock), // Unused.
contentDescription,
R.layout.empty_accessory_sheet, // Unused.
null); // Unused.
}
/**
......@@ -147,7 +115,7 @@ public class KeyboardAccessoryViewTest {
public void testClickableActionAddedWhenChangingModel() {
final AtomicReference<Boolean> buttonClicked = new AtomicReference<>();
final KeyboardAccessoryData.Action testAction =
createTestAction("Test Button", action -> buttonClicked.set(true));
new KeyboardAccessoryData.Action("Test Button", action -> buttonClicked.set(true));
ThreadUtils.runOnUiThreadBlocking(() -> {
mModel.setVisible(true);
......@@ -165,9 +133,9 @@ public class KeyboardAccessoryViewTest {
public void testCanAddSingleButtons() {
ThreadUtils.runOnUiThreadBlocking(() -> {
mModel.setVisible(true);
mModel.getActionList().set(
new KeyboardAccessoryData.Action[] {createTestAction("First", action -> {}),
createTestAction("Second", action -> {})});
mModel.getActionList().set(new KeyboardAccessoryData.Action[] {
new KeyboardAccessoryData.Action("First", action -> {}),
new KeyboardAccessoryData.Action("Second", action -> {})});
});
onView(isRoot()).check((root, e) -> waitForView((ViewGroup) root, withText("First")));
......@@ -175,7 +143,9 @@ public class KeyboardAccessoryViewTest {
onView(withText("Second")).check(matches(isDisplayed()));
ThreadUtils.runOnUiThreadBlocking(
() -> mModel.getActionList().add(createTestAction("Third", action -> {})));
()
-> mModel.getActionList().add(
new KeyboardAccessoryData.Action("Third", action -> {})));
onView(isRoot()).check((root, e) -> waitForView((ViewGroup) root, withText("Third")));
onView(withText("First")).check(matches(isDisplayed()));
......@@ -188,10 +158,10 @@ public class KeyboardAccessoryViewTest {
public void testCanRemoveSingleButtons() {
ThreadUtils.runOnUiThreadBlocking(() -> {
mModel.setVisible(true);
mModel.getActionList().set(
new KeyboardAccessoryData.Action[] {createTestAction("First", action -> {}),
createTestAction("Second", action -> {}),
createTestAction("Third", action -> {})});
mModel.getActionList().set(new KeyboardAccessoryData.Action[] {
new KeyboardAccessoryData.Action("First", action -> {}),
new KeyboardAccessoryData.Action("Second", action -> {}),
new KeyboardAccessoryData.Action("Third", action -> {})});
});
onView(isRoot()).check((root, e) -> waitForView((ViewGroup) root, withText("First")));
......
......@@ -57,7 +57,8 @@ public class ManualFillingIntegrationTest {
Assert.assertNotNull("Accessory Sheet should have an instance.",
mActivityTestRule.getActivity()
.getManualFillingController()
.getAccessorySheetForTesting());
.getMediatorForTesting()
.getAccessorySheet());
}
@Test
......
......@@ -17,8 +17,6 @@ import static org.chromium.chrome.test.util.ViewUtils.VIEW_NULL;
import static org.chromium.chrome.test.util.ViewUtils.waitForView;
import static org.chromium.ui.base.LocalizationUtils.setRtlForTesting;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.PerformException;
......@@ -102,30 +100,11 @@ public class ManualFillingTestHelper {
* Creates and adds an empty tab without listener to keyboard accessory and sheet.
*/
public void createTestTab() {
mActivityTestRule.getActivity().getManualFillingController().addTab(
new KeyboardAccessoryData.Tab() {
@Override
public Drawable getIcon() {
return InstrumentationRegistry.getContext().getResources().getDrawable(
android.R.drawable.ic_lock_lock);
}
@Override
public String getContentDescription() {
return "TestTabDescription";
}
@Override
public int getTabLayout() {
return R.layout.empty_accessory_sheet;
}
@Nullable
@Override
public Listener getListener() {
return null;
}
});
mActivityTestRule.getActivity().getManualFillingController().getMediatorForTesting().addTab(
new KeyboardAccessoryData.Tab(
InstrumentationRegistry.getContext().getResources().getDrawable(
android.R.drawable.ic_lock_lock),
"TestTabDescription", R.layout.empty_accessory_sheet, null));
}
/**
......
// Copyright 2015 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 android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToPosition;
import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
import static android.support.test.espresso.matcher.ViewMatchers.withChild;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingTestHelper.selectTabAtPosition;
import static org.chromium.chrome.browser.autofill.keyboard_accessory.ManualFillingTestHelper.whenDisplayed;
import android.support.test.espresso.matcher.BoundedMatcher;
import android.support.test.filters.SmallTest;
import android.text.method.PasswordTransformationMethod;
import android.view.View;
import android.widget.TextView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.Callback;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Item;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* Integration tests for password accessory views. This integration test currently stops testing at
* the bridge - ideally, there should be an easy way to add a temporary account with temporary
* passwords.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordAccessoryIntegrationTest {
@Rule
public final ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeActivity.class);
private final ManualFillingTestHelper mHelper = new ManualFillingTestHelper(mActivityTestRule);
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY})
public void testPasswordSheetIsAvailable() throws InterruptedException {
mHelper.loadTestPage(false);
Assert.assertNotNull("Password Sheet should be bound to accessory sheet.",
mActivityTestRule.getActivity()
.getManualFillingController()
.getMediatorForTesting()
.getPasswordAccessorySheet());
}
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.EXPERIMENTAL_UI})
public void testPasswordSheetIsAvailableInExperimentalUi() throws InterruptedException {
mHelper.loadTestPage(false);
Assert.assertNotNull("Password Sheet should be bound to accessory sheet.",
mActivityTestRule.getActivity()
.getManualFillingController()
.getMediatorForTesting()
.getPasswordAccessorySheet());
}
@Test
@SmallTest
public void testPasswordSheetUnavailableWithoutFeature() throws InterruptedException {
mHelper.loadTestPage(false);
Assert.assertNull("Password Sheet should not have been created.",
mActivityTestRule.getActivity()
.getManualFillingController()
.getMediatorForTesting()
.getPasswordAccessorySheet());
}
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY})
public void testPasswordSheetDisplaysProvidedItems()
throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
provideItems(new Item[] {
createLabel("Passwords"), createSuggestion("mayapark@gmail.com", null),
createPassword("SomeHiddenPassword"),
createSuggestion("mayaelisabethmercedesgreenepark@googlemail.com", null),
createPassword("ExtremelyLongPasswordThatUsesQuiteSomeSpaceInTheSheet"),
});
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
mHelper.waitForKeyboard();
whenDisplayed(withId(R.id.tabs)).perform(selectTabAtPosition(0));
// Check that the provided elements are there.
whenDisplayed(withText("Passwords"));
whenDisplayed(withText("mayapark@gmail.com"));
whenDisplayed(withText("SomeHiddenPassword")).check(matches(isTransformed()));
// TODO(fhorschig): Figure out whether the long name should be cropped or wrapped.
}
@Test
@SmallTest
@EnableFeatures({ChromeFeatureList.PASSWORDS_KEYBOARD_ACCESSORY})
public void testPasswordSheetTriggersCallback() throws InterruptedException, TimeoutException {
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),
createPassword("SomeHiddenLongPassword"),
});
// 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("Passwords"))).perform(scrollToPosition(7));
whenDisplayed(withText("mayapark@gmail.com")).perform(click());
// The callback should have triggered and set the reference to the selected Item.
assertThat(clicked.get(), notNullValue());
assertThat(clicked.get().getCaption(), equalTo("mayapark@gmail.com"));
}
/**
* Matches any {@link TextView} which applies a {@link PasswordTransformationMethod}.
* @return The matcher checking the transformation method.
*/
private static Matcher<View> isTransformed() {
return new BoundedMatcher<View, TextView>(TextView.class) {
@Override
public boolean matchesSafely(TextView textView) {
return textView.getTransformationMethod() instanceof PasswordTransformationMethod;
}
@Override
public void describeTo(Description description) {
description.appendText("is a transformed password.");
}
};
}
/**
* Creates a provider, binds it to the accessory sheet like a bridge would do and notifies about
* the given |items|.
* @param items The items to be provided to the password accessory sheet.
*/
private void provideItems(Item[] items) {
KeyboardAccessoryData.PropertyProvider<Item> itemProvider =
new KeyboardAccessoryData.PropertyProvider<>();
mActivityTestRule.getActivity()
.getManualFillingController()
.getMediatorForTesting()
.getPasswordAccessorySheet()
.registerItemProvider(itemProvider);
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);
}
private static Item createPassword(String caption) {
return new Item(Item.TYPE_SUGGESTIONS, caption, "Description_" + caption, true, null);
}
}
......@@ -8,7 +8,6 @@ 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.v4.view.ViewPager;
......@@ -22,12 +21,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Item;
import org.chromium.chrome.browser.modelutil.SimpleListObservable;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
......@@ -43,7 +42,7 @@ import java.util.concurrent.atomic.AtomicReference;
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordAccessorySheetViewTest {
private SimpleListObservable<KeyboardAccessoryData.Item> mModel;
private SimpleListObservable<Item> mModel;
private AtomicReference<RecyclerView> mView = new AtomicReference<>();
@Rule
......@@ -68,24 +67,7 @@ public class PasswordAccessorySheetViewTest {
@Override
public void onPageScrollStateChanged(int i) {}
});
accessorySheet.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;
}
});
accessorySheet.addTab(new KeyboardAccessoryData.Tab(null, null, layout, listener));
ThreadUtils.runOnUiThreadBlocking(accessorySheet::show);
}
......@@ -113,32 +95,8 @@ public class PasswordAccessorySheetViewTest {
public void testAddingCaptionsToTheModelRendersThem() {
assertThat(mView.get().getChildCount(), is(0));
ThreadUtils.runOnUiThreadBlocking(() -> mModel.add(new KeyboardAccessoryData.Item() {
@Override
public int getType() {
return KeyboardAccessoryData.Item.TYPE_LABEL;
}
@Override
public String getCaption() {
return "Passwords";
}
@Override
public String getContentDescription() {
return null;
}
@Override
public boolean isPassword() {
return false;
}
@Override
public Callback<KeyboardAccessoryData.Item> getItemSelectedCallback() {
return null;
}
}));
ThreadUtils.runOnUiThreadBlocking(
() -> mModel.add(new Item(Item.TYPE_LABEL, "Passwords", null, false, null)));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
......@@ -151,32 +109,10 @@ public class PasswordAccessorySheetViewTest {
final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
assertThat(mView.get().getChildCount(), is(0));
ThreadUtils.runOnUiThreadBlocking(() -> mModel.add(new KeyboardAccessoryData.Item() {
@Override
public int getType() {
return KeyboardAccessoryData.Item.TYPE_SUGGESTIONS;
}
@Override
public String getCaption() {
return "Name Suggestion";
}
@Override
public String getContentDescription() {
return null;
}
@Override
public boolean isPassword() {
return false;
}
@Override
public Callback<KeyboardAccessoryData.Item> getItemSelectedCallback() {
return item -> clicked.set(true);
}
}));
ThreadUtils.runOnUiThreadBlocking(
()
-> mModel.add(new Item(Item.TYPE_SUGGESTIONS, "Name Suggestion", null,
false, item -> clicked.set(true))));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
......@@ -194,32 +130,10 @@ public class PasswordAccessorySheetViewTest {
final AtomicReference<Boolean> clicked = new AtomicReference<>(false);
assertThat(mView.get().getChildCount(), is(0));
ThreadUtils.runOnUiThreadBlocking(() -> mModel.add(new KeyboardAccessoryData.Item() {
@Override
public int getType() {
return KeyboardAccessoryData.Item.TYPE_SUGGESTIONS;
}
@Override
public String getCaption() {
return "Password Suggestion";
}
@Override
public String getContentDescription() {
return null;
}
@Override
public boolean isPassword() {
return true;
}
@Override
public Callback<KeyboardAccessoryData.Item> getItemSelectedCallback() {
return item -> clicked.set(true);
}
}));
ThreadUtils.runOnUiThreadBlocking(
()
-> mModel.add(new Item(Item.TYPE_SUGGESTIONS, "Password Suggestion", null,
true, item -> clicked.set(true))));
CriteriaHelper.pollUiThread(Criteria.equals(1, () -> mView.get().getChildCount()));
assertThat(mView.get().getChildAt(0), instanceOf(TextView.class));
......
......@@ -24,6 +24,7 @@ import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.autofill.keyboard_accessory.AccessorySheetModel.PropertyKey;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Tab;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable;
......@@ -41,14 +42,10 @@ public class AccessorySheetControllerTest {
private ViewStub mMockViewStub;
@Mock
private ViewPager mMockView;
@Mock
private KeyboardAccessoryData.Tab mMockTab;
@Mock
private KeyboardAccessoryData.Tab mSecondMockTab;
@Mock
private KeyboardAccessoryData.Tab mThirdMockTab;
@Mock
private KeyboardAccessoryData.Tab mFourthMockTab;
private final Tab[] mTabs =
new Tab[] {new Tab(null, null, 0, null), new Tab(null, null, 0, null),
new Tab(null, null, 0, null), new Tab(null, null, 0, null)};
private AccessorySheetCoordinator mCoordinator;
private AccessorySheetMediator mMediator;
......@@ -96,7 +93,7 @@ public class AccessorySheetControllerTest {
mModel.getTabList().addObserver(mTabListObserver);
assertThat(mModel.getTabList().getItemCount(), is(0));
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mTabs[0]);
verify(mTabListObserver).onItemRangeInserted(mModel.getTabList(), 0, 1);
assertThat(mModel.getTabList().getItemCount(), is(1));
}
......@@ -110,14 +107,14 @@ public class AccessorySheetControllerTest {
assertThat(mCoordinator.getTab(), is(nullValue()));
// The first tab becomes the active Tab.
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mTabs[0]);
verify(mMockPropertyObserver).onPropertyChanged(mModel, PropertyKey.ACTIVE_TAB_INDEX);
assertThat(mModel.getTabList().getItemCount(), is(1));
assertThat(mModel.getActiveTabIndex(), is(0));
assertThat(mCoordinator.getTab(), is(mMockTab));
assertThat(mCoordinator.getTab(), is(mTabs[0]));
// A second tab is added but doesn't become automatically active.
mCoordinator.addTab(mSecondMockTab);
mCoordinator.addTab(mTabs[1]);
verify(mMockPropertyObserver).onPropertyChanged(mModel, PropertyKey.ACTIVE_TAB_INDEX);
assertThat(mModel.getTabList().getItemCount(), is(2));
assertThat(mModel.getActiveTabIndex(), is(0));
......@@ -125,14 +122,14 @@ public class AccessorySheetControllerTest {
@Test
public void testDeletingFirstTabActivatesNewFirstTab() {
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mSecondMockTab);
mCoordinator.addTab(mThirdMockTab);
mCoordinator.addTab(mFourthMockTab);
mCoordinator.addTab(mTabs[0]);
mCoordinator.addTab(mTabs[1]);
mCoordinator.addTab(mTabs[2]);
mCoordinator.addTab(mTabs[3]);
assertThat(mModel.getTabList().getItemCount(), is(4));
assertThat(mModel.getActiveTabIndex(), is(0));
mCoordinator.removeTab(mMockTab);
mCoordinator.removeTab(mTabs[0]);
assertThat(mModel.getTabList().getItemCount(), is(3));
assertThat(mModel.getActiveTabIndex(), is(0));
......@@ -140,8 +137,8 @@ public class AccessorySheetControllerTest {
@Test
public void testDeletingFirstAndOnlyTabInvalidatesActiveTab() {
mCoordinator.addTab(mMockTab);
mCoordinator.removeTab(mMockTab);
mCoordinator.addTab(mTabs[0]);
mCoordinator.removeTab(mTabs[0]);
assertThat(mModel.getTabList().getItemCount(), is(0));
assertThat(mModel.getActiveTabIndex(), is(AccessorySheetModel.NO_ACTIVE_TAB));
......@@ -149,14 +146,14 @@ public class AccessorySheetControllerTest {
@Test
public void testDeletedActiveTabDisappearsAndActivatesLeftNeighbor() {
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mSecondMockTab);
mCoordinator.addTab(mThirdMockTab);
mCoordinator.addTab(mFourthMockTab);
mCoordinator.addTab(mTabs[0]);
mCoordinator.addTab(mTabs[1]);
mCoordinator.addTab(mTabs[2]);
mCoordinator.addTab(mTabs[3]);
mModel.setActiveTabIndex(2);
mModel.addObserver(mMockPropertyObserver);
mCoordinator.removeTab(mThirdMockTab);
mCoordinator.removeTab(mTabs[2]);
verify(mMockPropertyObserver).onPropertyChanged(mModel, PropertyKey.ACTIVE_TAB_INDEX);
assertThat(mModel.getTabList().getItemCount(), is(3));
......@@ -165,14 +162,14 @@ public class AccessorySheetControllerTest {
@Test
public void testCorrectsPositionOfActiveTabForDeletedPredecessors() {
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mSecondMockTab);
mCoordinator.addTab(mThirdMockTab);
mCoordinator.addTab(mFourthMockTab);
mCoordinator.addTab(mTabs[0]);
mCoordinator.addTab(mTabs[1]);
mCoordinator.addTab(mTabs[2]);
mCoordinator.addTab(mTabs[3]);
mModel.setActiveTabIndex(2);
mModel.addObserver(mMockPropertyObserver);
mCoordinator.removeTab(mSecondMockTab);
mCoordinator.removeTab(mTabs[1]);
verify(mMockPropertyObserver).onPropertyChanged(mModel, PropertyKey.ACTIVE_TAB_INDEX);
assertThat(mModel.getTabList().getItemCount(), is(3));
......@@ -181,13 +178,13 @@ public class AccessorySheetControllerTest {
@Test
public void testDoesntChangePositionOfActiveTabForDeletedSuccessors() {
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mSecondMockTab);
mCoordinator.addTab(mThirdMockTab);
mCoordinator.addTab(mFourthMockTab);
mCoordinator.addTab(mTabs[0]);
mCoordinator.addTab(mTabs[1]);
mCoordinator.addTab(mTabs[2]);
mCoordinator.addTab(mTabs[3]);
mModel.setActiveTabIndex(2);
mCoordinator.removeTab(mFourthMockTab);
mCoordinator.removeTab(mTabs[3]);
assertThat(mModel.getTabList().getItemCount(), is(3));
assertThat(mModel.getActiveTabIndex(), is(2));
......
......@@ -53,19 +53,8 @@ public class KeyboardAccessoryControllerTest {
@Mock
private KeyboardAccessoryView mMockView;
@Mock
private KeyboardAccessoryData.Tab mMockTab;
private static class FakeAction implements KeyboardAccessoryData.Action {
@Override
public String getCaption() {
return null;
}
@Override
public Delegate getDelegate() {
return null;
}
}
private final KeyboardAccessoryData.Tab mTestTab =
new KeyboardAccessoryData.Tab(null, null, 0, null);
private KeyboardAccessoryCoordinator mCoordinator;
private KeyboardAccessoryModel mModel;
......@@ -111,13 +100,13 @@ public class KeyboardAccessoryControllerTest {
mModel.addTabListObserver(mMockTabListObserver);
// Calling addTab on the coordinator should make model propagate that it has a new tab.
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mTestTab);
verify(mMockTabListObserver).onItemRangeInserted(mModel.getTabList(), 0, 1);
assertThat(mModel.getTabList().getItemCount(), is(1));
assertThat(mModel.getTabList().get(0), is(mMockTab));
assertThat(mModel.getTabList().get(0), is(mTestTab));
// Calling hide on the coordinator should make model propagate that it's invisible.
mCoordinator.removeTab(mMockTab);
mCoordinator.removeTab(mTestTab);
verify(mMockTabListObserver).onItemRangeRemoved(mModel.getTabList(), 0, 1);
assertThat(mModel.getTabList().getItemCount(), is(0));
}
......@@ -125,7 +114,7 @@ public class KeyboardAccessoryControllerTest {
@Test
public void testModelNotifiesAboutActionsChangedByProvider() {
final PropertyProvider<Action> testProvider = new PropertyProvider<>();
final FakeAction testAction = new FakeAction();
final Action testAction = new Action(null, null);
mModel.addActionListObserver(mMockActionListObserver);
mCoordinator.registerActionListProvider(testProvider);
......@@ -210,7 +199,7 @@ public class KeyboardAccessoryControllerTest {
assertThat(mModel.isVisible(), is(false));
// Adding actions while the keyboard is visible triggers the accessory.
mModel.getActionList().add(new FakeAction());
mModel.getActionList().add(new Action(null, null));
assertThat(mModel.isVisible(), is(true));
}
......@@ -222,7 +211,7 @@ public class KeyboardAccessoryControllerTest {
assertThat(mModel.isVisible(), is(false));
// Adding actions while the keyboard is visible triggers the accessory.
mCoordinator.addTab(mMockTab);
mCoordinator.addTab(mTestTab);
assertThat(mModel.isVisible(), is(true));
}
}
\ No newline at end of file
......@@ -22,7 +22,6 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.chromium.base.Callback;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.SimpleListObservable;
......@@ -51,7 +50,7 @@ public class PasswordAccessorySheetControllerTest {
@Test
public void testCreatesValidTab() {
KeyboardAccessoryData.Tab tab = mCoordinator.createTab();
KeyboardAccessoryData.Tab tab = mCoordinator.getTab();
assertNotNull(tab);
assertNotNull(tab.getIcon());
assertNotNull(tab.getListener());
......@@ -59,7 +58,7 @@ public class PasswordAccessorySheetControllerTest {
@Test
public void testSetsViewAdapterOnTabCreation() {
KeyboardAccessoryData.Tab tab = mCoordinator.createTab();
KeyboardAccessoryData.Tab tab = mCoordinator.getTab();
assertNotNull(tab);
assertNotNull(tab.getListener());
tab.getListener().onTabCreated(mMockView);
......@@ -70,29 +69,8 @@ public class PasswordAccessorySheetControllerTest {
public void testModelNotifiesAboutActionsChangedByProvider() {
final KeyboardAccessoryData.PropertyProvider<KeyboardAccessoryData.Item> testProvider =
new KeyboardAccessoryData.PropertyProvider<>();
final KeyboardAccessoryData.Item testItem = new KeyboardAccessoryData.Item() {
@Override
public int getType() {
return KeyboardAccessoryData.Item.TYPE_LABEL;
}
@Override
public String getCaption() {
return "Test Item";
}
@Override
public String getContentDescription() {
return null;
}
@Override
public boolean isPassword() {
return false;
}
@Override
public Callback<KeyboardAccessoryData.Item> getItemSelectedCallback() {
return null;
}
};
final KeyboardAccessoryData.Item testItem = new KeyboardAccessoryData.Item(
KeyboardAccessoryData.Item.TYPE_LABEL, "Test Item", null, false, null);
mModel.addObserver(mMockItemListObserver);
mCoordinator.registerItemProvider(testProvider);
......
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