Commit 5179909c authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

[Android] Wire accessory sheet to keyboard accessory

This CL connects the TabLayout int the keyboard accessory with the
ViewPager in the accessory sheet.
This means the visibility of keyboard and tabs can be controlled by the
ManualFillingCoordinator and is decoupled from the accessory.

Bug: 828832, 811747
Change-Id: I01f225458d068e75fde19398fc352672779d23c0
Reviewed-on: https://chromium-review.googlesource.com/1091190
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#567196}
parent 7121bff4
...@@ -9,6 +9,7 @@ import android.support.v4.view.PagerAdapter; ...@@ -9,6 +9,7 @@ import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.view.ViewStub; import android.view.ViewStub;
import org.chromium.base.Supplier;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter; import org.chromium.chrome.browser.modelutil.LazyViewBinderAdapter;
import org.chromium.chrome.browser.modelutil.ListModelChangeProcessor; import org.chromium.chrome.browser.modelutil.ListModelChangeProcessor;
...@@ -22,18 +23,23 @@ import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor; ...@@ -22,18 +23,23 @@ import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor;
*/ */
public class AccessorySheetCoordinator { public class AccessorySheetCoordinator {
private final AccessorySheetMediator mMediator; private final AccessorySheetMediator mMediator;
private final Supplier<ViewPager.OnPageChangeListener> mProvider;
/** /**
* Creates the sheet component by instantiating Model, View and Controller before wiring these * Creates the sheet component by instantiating Model, View and Controller before wiring these
* parts up. * parts up.
* @param viewStub The view stub that can be inflated into the accessory layout. * @param viewStub The view stub that can be inflated into the accessory layout.
* @param provider The provider of a {@link ViewPager.OnPageChangeListener} used for navigation.
*/ */
public AccessorySheetCoordinator(ViewStub viewStub) { public AccessorySheetCoordinator(
ViewStub viewStub, Supplier<ViewPager.OnPageChangeListener> provider) {
mProvider = provider;
LazyViewBinderAdapter.StubHolder<ViewPager> stubHolder = LazyViewBinderAdapter.StubHolder<ViewPager> stubHolder =
new LazyViewBinderAdapter.StubHolder<>(viewStub); new LazyViewBinderAdapter.StubHolder<>(viewStub);
AccessorySheetModel model = new AccessorySheetModel(); AccessorySheetModel model = new AccessorySheetModel();
model.addObserver(new PropertyModelChangeProcessor<>( model.addObserver(new PropertyModelChangeProcessor<>(model, stubHolder,
model, stubHolder, new LazyViewBinderAdapter<>(new AccessorySheetViewBinder()))); new LazyViewBinderAdapter<>(
new AccessorySheetViewBinder(), this::onViewInflated)));
mMediator = new AccessorySheetMediator(model); mMediator = new AccessorySheetMediator(model);
} }
...@@ -50,6 +56,15 @@ public class AccessorySheetCoordinator { ...@@ -50,6 +56,15 @@ public class AccessorySheetCoordinator {
return adapter; return adapter;
} }
/**
* Called by the {@link LazyViewBinderAdapter} as soon as the view is inflated so it can be
* initialized. This call happens before the {@link AccessorySheetViewBinder} is called for the
* first time.
*/
private void onViewInflated(ViewPager view) {
view.addOnPageChangeListener(mProvider.get());
}
/** /**
* Adds the contents of a given {@link KeyboardAccessoryData.Tab} to the accessory sheet. If it * Adds the contents of a given {@link KeyboardAccessoryData.Tab} to the accessory sheet. If it
* is the first Tab, it automatically becomes the active Tab. * is the first Tab, it automatically becomes the active Tab.
...@@ -74,8 +89,38 @@ public class AccessorySheetCoordinator { ...@@ -74,8 +89,38 @@ public class AccessorySheetCoordinator {
return mMediator.getTab(); return mMediator.getTab();
} }
/**
* Shows the Accessory Sheet.
*/
public void show() {
mMediator.show();
}
/**
* Hides the Accessory Sheet.
*/
public void hide() {
mMediator.hide();
}
/**
* Returns whether the accessory sheet is currently visible.
* @return True, if the accessory sheet is visible.
*/
public boolean isShown() {
return mMediator.isShown();
}
@VisibleForTesting @VisibleForTesting
AccessorySheetMediator getMediatorForTesting() { AccessorySheetMediator getMediatorForTesting() {
return mMediator; return mMediator;
} }
/**
* Calling this function changes the active tab to the tab at the given |position|.
* @param position The index of the tab (starting with 0) that should be set active.
*/
public void setActiveTab(int position) {
mMediator.setActiveTab(position);
}
} }
\ No newline at end of file
...@@ -40,6 +40,10 @@ class AccessorySheetMediator { ...@@ -40,6 +40,10 @@ class AccessorySheetMediator {
mModel.setVisible(false); mModel.setVisible(false);
} }
boolean isShown() {
return mModel.isVisible();
}
void addTab(KeyboardAccessoryData.Tab tab) { void addTab(KeyboardAccessoryData.Tab tab) {
mModel.getTabList().add(tab); mModel.getTabList().add(tab);
if (mModel.getActiveTabIndex() == NO_ACTIVE_TAB) { if (mModel.getActiveTabIndex() == NO_ACTIVE_TAB) {
...@@ -51,6 +55,13 @@ class AccessorySheetMediator { ...@@ -51,6 +55,13 @@ class AccessorySheetMediator {
assert mModel.getActiveTabIndex() != NO_ACTIVE_TAB; assert mModel.getActiveTabIndex() != NO_ACTIVE_TAB;
mModel.setActiveTabIndex(getNextActiveTab(tab)); mModel.setActiveTabIndex(getNextActiveTab(tab));
mModel.getTabList().remove(tab); mModel.getTabList().remove(tab);
if (mModel.getActiveTabIndex() == NO_ACTIVE_TAB) hide();
}
void setActiveTab(int position) {
assert position < mModel.getTabList().getItemCount()
|| position >= 0 : position + " is not a valid tab index!";
mModel.setActiveTabIndex(position);
} }
/** /**
......
...@@ -43,7 +43,6 @@ class AccessorySheetViewBinder ...@@ -43,7 +43,6 @@ class AccessorySheetViewBinder
if (propertyKey == PropertyKey.ACTIVE_TAB_INDEX) { if (propertyKey == PropertyKey.ACTIVE_TAB_INDEX) {
if (model.getActiveTabIndex() != AccessorySheetModel.NO_ACTIVE_TAB) { if (model.getActiveTabIndex() != AccessorySheetModel.NO_ACTIVE_TAB) {
inflatedView.setCurrentItem(model.getActiveTabIndex()); inflatedView.setCurrentItem(model.getActiveTabIndex());
// inflatedView.post(() -> inflatedView.setCurrentItem(model.getActiveTabIndex()));
} }
return; return;
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory; package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.v4.view.ViewPager;
import android.view.ViewStub; import android.view.ViewStub;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
...@@ -17,7 +18,6 @@ import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor; ...@@ -17,7 +18,6 @@ import org.chromium.chrome.browser.modelutil.PropertyModelChangeProcessor;
import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter; import org.chromium.chrome.browser.modelutil.RecyclerViewAdapter;
import org.chromium.chrome.browser.modelutil.RecyclerViewModelChangeProcessor; import org.chromium.chrome.browser.modelutil.RecyclerViewModelChangeProcessor;
import org.chromium.chrome.browser.modelutil.SimpleListObservable; import org.chromium.chrome.browser.modelutil.SimpleListObservable;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
/** /**
...@@ -29,19 +29,51 @@ public class KeyboardAccessoryCoordinator { ...@@ -29,19 +29,51 @@ public class KeyboardAccessoryCoordinator {
private final KeyboardAccessoryMediator mMediator; private final KeyboardAccessoryMediator mMediator;
private LazyViewBinderAdapter.StubHolder<KeyboardAccessoryView> mViewHolder; private LazyViewBinderAdapter.StubHolder<KeyboardAccessoryView> mViewHolder;
/**
* The keyboard accessory provides signals when to show or change the accessory sheet below it.
* The actual implementation isn't relevant for this component. Therefore, a class implementing
* this interface takes that responsibility, i.e. {@link ManualFillingCoordinator}.
*/
public interface VisibilityDelegate {
/**
* Is triggered when a tab in the accessory was selected and the sheet needs to change.
* If the sheet needs to also be opened, {@link VisibilityDelegate#onOpenAccessorySheet()}
* is called right after this method.
* @param tabIndex The index of the selected tab in the tab bar.
*/
void onChangeAccessorySheet(int tabIndex);
/**
* Called when the sheet needs to be opened.
*/
void onOpenAccessorySheet();
/**
* Called when the sheet needs to be hidden.
*/
void onCloseAccessorySheet();
/**
* Called when the whole accessory should be hidden (e.g. due to selecting a suggestion).
*/
void onCloseKeyboardAccessory();
}
/** /**
* Initializes the component as soon as the native library is loaded by e.g. starting to listen * Initializes the component as soon as the native library is loaded by e.g. starting to listen
* to keyboard visibility events. * to keyboard visibility events.
* @param windowAndroid The window connected to the activity this component lives in. * @param windowAndroid The window connected to the activity this component lives in.
* @param viewStub the stub that will become the accessory. * @param viewStub the stub that will become the accessory.
*/ */
public KeyboardAccessoryCoordinator(WindowAndroid windowAndroid, ViewStub viewStub) { public KeyboardAccessoryCoordinator(
WindowAndroid windowAndroid, ViewStub viewStub, VisibilityDelegate visibilityDelegate) {
KeyboardAccessoryModel model = new KeyboardAccessoryModel(); KeyboardAccessoryModel model = new KeyboardAccessoryModel();
mMediator = new KeyboardAccessoryMediator(model, windowAndroid); mMediator = new KeyboardAccessoryMediator(model, windowAndroid, visibilityDelegate);
mViewHolder = new LazyViewBinderAdapter.StubHolder<>(viewStub); mViewHolder = new LazyViewBinderAdapter.StubHolder<>(viewStub);
model.addObserver(new PropertyModelChangeProcessor<>(model, mViewHolder, model.addObserver(new PropertyModelChangeProcessor<>(model, mViewHolder,
new LazyViewBinderAdapter<>(new KeyboardAccessoryViewBinder()))); new LazyViewBinderAdapter<>(
new KeyboardAccessoryViewBinder(), this::onViewInflated)));
} }
/** /**
...@@ -74,6 +106,16 @@ public class KeyboardAccessoryCoordinator { ...@@ -74,6 +106,16 @@ public class KeyboardAccessoryCoordinator {
return tabViewBinder; return tabViewBinder;
} }
/**
* Called by the {@link LazyViewBinderAdapter} as soon as the view is inflated so it can be
* initialized. This call happens before the {@link KeyboardAccessoryViewBinder} is called for
* the first time.
* @param view The view that was inflated from the initially given {@link ViewStub}.
*/
private void onViewInflated(KeyboardAccessoryView view) {
view.setTabSelectionAdapter(mMediator);
}
/** /**
* A {@link KeyboardAccessoryData.Tab} passed into this function will be represented as item at * A {@link KeyboardAccessoryData.Tab} passed into this function will be represented as item at
* the start of the accessory. It is meant to trigger various bottom sheets. * the start of the accessory. It is meant to trigger various bottom sheets.
...@@ -125,12 +167,19 @@ public class KeyboardAccessoryCoordinator { ...@@ -125,12 +167,19 @@ public class KeyboardAccessoryCoordinator {
*/ */
public void dismiss() { public void dismiss() {
mMediator.dismiss(); mMediator.dismiss();
// TODO(fhorschig): Move hiding to upcoming root controller, drop it or reassign to bauerb@.
UiUtils.hideKeyboard(mViewHolder.getView());
} }
@VisibleForTesting @VisibleForTesting
KeyboardAccessoryMediator getMediatorForTesting() { KeyboardAccessoryMediator getMediatorForTesting() {
return mMediator; return mMediator;
} }
/**
* Provides the PageChangeListener that is needed to wire up the TabLayout with the ViewPager.
* @return Returns a {@link ViewPager.OnPageChangeListener}.
*/
ViewPager.OnPageChangeListener getPageChangeListener() {
assert mViewHolder.getView() != null : "Requested PageChangeListener before inflation.";
return mViewHolder.getView().getPageChangeListener();
}
} }
...@@ -5,9 +5,11 @@ ...@@ -5,9 +5,11 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory; package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions; import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryCoordinator.VisibilityDelegate;
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable; import org.chromium.chrome.browser.modelutil.PropertyObservable;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
...@@ -23,24 +25,29 @@ import org.chromium.ui.base.WindowAndroid; ...@@ -23,24 +25,29 @@ import org.chromium.ui.base.WindowAndroid;
class KeyboardAccessoryMediator class KeyboardAccessoryMediator
implements WindowAndroid.KeyboardVisibilityListener, ListObservable.ListObserver<Void>, implements WindowAndroid.KeyboardVisibilityListener, ListObservable.ListObserver<Void>,
PropertyObservable.PropertyObserver<KeyboardAccessoryModel.PropertyKey>, PropertyObservable.PropertyObserver<KeyboardAccessoryModel.PropertyKey>,
KeyboardAccessoryData.Observer<KeyboardAccessoryData.Action> { KeyboardAccessoryData.Observer<KeyboardAccessoryData.Action>,
TabLayout.OnTabSelectedListener {
private final KeyboardAccessoryModel mModel; private final KeyboardAccessoryModel mModel;
private final WindowAndroid mWindowAndroid; private final WindowAndroid mWindowAndroid;
private final VisibilityDelegate mVisibilityDelegate;
// TODO(fhorschig): Look stronger signals than |keyboardVisibilityChanged|. // TODO(fhorschig): Look for stronger signals than |keyboardVisibilityChanged|.
// This variable remembers the last state of |keyboardVisibilityChanged| which might not be // This variable remembers the last state of |keyboardVisibilityChanged| which might not be
// sufficient for edge cases like hardware keyboards, floating keyboards, etc. // sufficient for edge cases like hardware keyboards, floating keyboards, etc.
private boolean mIsKeyboardVisible; private boolean mIsKeyboardVisible;
KeyboardAccessoryMediator(KeyboardAccessoryModel model, WindowAndroid windowAndroid) { KeyboardAccessoryMediator(KeyboardAccessoryModel model, WindowAndroid windowAndroid,
VisibilityDelegate visibilityDelegate) {
mModel = model; mModel = model;
mWindowAndroid = windowAndroid; mWindowAndroid = windowAndroid;
mVisibilityDelegate = visibilityDelegate;
windowAndroid.addKeyboardVisibilityListener(this); windowAndroid.addKeyboardVisibilityListener(this);
// Add mediator as observer so it can use model changes as signal for accessory visibility. // Add mediator as observer so it can use model changes as signal for accessory visibility.
mModel.addObserver(this); mModel.addObserver(this);
mModel.getTabList().addObserver(this); mModel.getTabList().addObserver(this);
mModel.getActionList().addObserver(this); mModel.getActionList().addObserver(this);
mModel.setTabSelectionCallbacks(this);
} }
void destroy() { void destroy() {
...@@ -71,6 +78,7 @@ class KeyboardAccessoryMediator ...@@ -71,6 +78,7 @@ class KeyboardAccessoryMediator
} }
void dismiss() { void dismiss() {
mVisibilityDelegate.onCloseKeyboardAccessory();
if (mModel.getAutofillSuggestions() == null) return; // Nothing to do here. if (mModel.getAutofillSuggestions() == null) return; // Nothing to do here.
mModel.getAutofillSuggestions().dismiss(); mModel.getAutofillSuggestions().dismiss();
mModel.setAutofillSuggestions(null); mModel.setAutofillSuggestions(null);
...@@ -105,7 +113,17 @@ class KeyboardAccessoryMediator ...@@ -105,7 +113,17 @@ class KeyboardAccessoryMediator
public void onPropertyChanged(PropertyObservable<KeyboardAccessoryModel.PropertyKey> source, public void onPropertyChanged(PropertyObservable<KeyboardAccessoryModel.PropertyKey> source,
@Nullable KeyboardAccessoryModel.PropertyKey propertyKey) { @Nullable KeyboardAccessoryModel.PropertyKey propertyKey) {
// Update the visibility only if we haven't set it just now. // Update the visibility only if we haven't set it just now.
if (propertyKey == KeyboardAccessoryModel.PropertyKey.VISIBLE) return; if (propertyKey == KeyboardAccessoryModel.PropertyKey.VISIBLE) {
// When the accessory just (dis)appeared, there should be no active tab.
mModel.setActiveTab(null);
return;
}
if (propertyKey == KeyboardAccessoryModel.PropertyKey.ACTIVE_TAB) {
return;
}
if (propertyKey == KeyboardAccessoryModel.PropertyKey.TAB_SELECTION_CALLBACKS) {
return;
}
if (propertyKey == KeyboardAccessoryModel.PropertyKey.SUGGESTIONS) { if (propertyKey == KeyboardAccessoryModel.PropertyKey.SUGGESTIONS) {
updateVisibility(); updateVisibility();
return; return;
...@@ -113,8 +131,31 @@ class KeyboardAccessoryMediator ...@@ -113,8 +131,31 @@ class KeyboardAccessoryMediator
assert false : "Every property update needs to be handled explicitly!"; assert false : "Every property update needs to be handled explicitly!";
} }
@Override
public void onTabSelected(TabLayout.Tab tab) {
boolean hadNoActiveTab = mModel.activeTab() == null;
mModel.setActiveTab(tab.getPosition());
mVisibilityDelegate.onChangeAccessorySheet(tab.getPosition());
if (hadNoActiveTab) mVisibilityDelegate.onOpenAccessorySheet();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
if (mModel.activeTab() == null) {
mModel.setActiveTab(tab.getPosition());
mVisibilityDelegate.onChangeAccessorySheet(tab.getPosition());
mVisibilityDelegate.onOpenAccessorySheet();
} else {
mModel.setActiveTab(null);
mVisibilityDelegate.onCloseAccessorySheet();
}
}
private boolean shouldShowAccessory() { private boolean shouldShowAccessory() {
if (!mIsKeyboardVisible) return false; if (!mIsKeyboardVisible && mModel.activeTab() == null) return false;
return mModel.getAutofillSuggestions() != null || mModel.getActionList().getItemCount() > 0 return mModel.getAutofillSuggestions() != null || mModel.getActionList().getItemCount() > 0
|| mModel.getTabList().getItemCount() > 0; || mModel.getTabList().getItemCount() > 0;
} }
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory; package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions; import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable; import org.chromium.chrome.browser.modelutil.PropertyObservable;
...@@ -28,6 +31,8 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P ...@@ -28,6 +31,8 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P
static final PropertyKey VISIBLE = new PropertyKey(); static final PropertyKey VISIBLE = new PropertyKey();
static final PropertyKey SUGGESTIONS = new PropertyKey(); static final PropertyKey SUGGESTIONS = new PropertyKey();
static final PropertyKey ACTIVE_TAB = new PropertyKey();
static final PropertyKey TAB_SELECTION_CALLBACKS = new PropertyKey();
private PropertyKey() { private PropertyKey() {
ALL_PROPERTIES.add(this); ALL_PROPERTIES.add(this);
...@@ -37,6 +42,8 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P ...@@ -37,6 +42,8 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P
private SimpleListObservable<KeyboardAccessoryData.Action> mActionListObservable; private SimpleListObservable<KeyboardAccessoryData.Action> mActionListObservable;
private SimpleListObservable<KeyboardAccessoryData.Tab> mTabListObservable; private SimpleListObservable<KeyboardAccessoryData.Tab> mTabListObservable;
private boolean mVisible; private boolean mVisible;
private @Nullable Integer mActiveTab;
private TabLayout.OnTabSelectedListener mTabSelectionCallbacks;
// TODO(fhorschig): Ideally, make this a ListObservable populating a RecyclerView. // TODO(fhorschig): Ideally, make this a ListObservable populating a RecyclerView.
private AutofillKeyboardSuggestions mAutofillSuggestions; private AutofillKeyboardSuggestions mAutofillSuggestions;
...@@ -84,6 +91,27 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P ...@@ -84,6 +91,27 @@ class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.P
return mVisible; return mVisible;
} }
void setActiveTab(Integer activeTab) {
if (null != mActiveTab && mActiveTab.equals(activeTab)) return;
mActiveTab = activeTab;
notifyPropertyChanged(PropertyKey.ACTIVE_TAB);
}
@Nullable
Integer activeTab() {
return mActiveTab;
}
TabLayout.OnTabSelectedListener getTabSelectionCallbacks() {
return mTabSelectionCallbacks;
}
void setTabSelectionCallbacks(TabLayout.OnTabSelectedListener tabSelectionCallbacks) {
if (tabSelectionCallbacks == mTabSelectionCallbacks) return; // Nothing to do: same object.
mTabSelectionCallbacks = tabSelectionCallbacks;
notifyPropertyChanged(PropertyKey.TAB_SELECTION_CALLBACKS);
}
AutofillKeyboardSuggestions getAutofillSuggestions() { AutofillKeyboardSuggestions getAutofillSuggestions() {
return mAutofillSuggestions; return mAutofillSuggestions;
} }
......
...@@ -7,9 +7,12 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory; ...@@ -7,9 +7,12 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory;
import static org.chromium.ui.base.LocalizationUtils.isLayoutRtl; import static org.chromium.ui.base.LocalizationUtils.isLayoutRtl;
import android.content.Context; import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet; import android.util.AttributeSet;
...@@ -33,6 +36,7 @@ class KeyboardAccessoryView extends LinearLayout { ...@@ -33,6 +36,7 @@ class KeyboardAccessoryView extends LinearLayout {
private HorizontalScrollView mSuggestionsView; private HorizontalScrollView mSuggestionsView;
private RecyclerView mActionsView; private RecyclerView mActionsView;
private TabLayout mTabLayout; private TabLayout mTabLayout;
private TabLayout.TabLayoutOnPageChangeListener mPageChangeListener;
private static class HorizontalDividerItemDecoration extends RecyclerView.ItemDecoration { private static class HorizontalDividerItemDecoration extends RecyclerView.ItemDecoration {
private final int mHorizontalMargin; private final int mHorizontalMargin;
...@@ -95,21 +99,49 @@ class KeyboardAccessoryView extends LinearLayout { ...@@ -95,21 +99,49 @@ class KeyboardAccessoryView extends LinearLayout {
* @param contentDescription The contentDescription to be used for the tab icon. * @param contentDescription The contentDescription to be used for the tab icon.
*/ */
void addTabAt(int position, Drawable icon, CharSequence contentDescription) { void addTabAt(int position, Drawable icon, CharSequence contentDescription) {
this.post(() -> { // Using |post| ensures this is cannot happen before inflation finishes.
TabLayout.Tab tab = mTabLayout.newTab(); TabLayout.Tab tab = mTabLayout.newTab();
tab.setIcon(icon); // TODO(fhorschig): Call .mutate() when changing the active tint. tab.setIcon(icon.mutate()); // mutate() needed to change the active tint.
tab.setContentDescription(contentDescription); tab.setContentDescription(contentDescription);
mTabLayout.addTab(tab, position); mTabLayout.addTab(tab, position, false);
});
} }
void removeTabAt(int position) { void removeTabAt(int position) {
mTabLayout.removeTabAt(position); this.post(() -> mTabLayout.removeTabAt(position));
} }
/** /**
* Removes all tabs. * Removes all tabs.
*/ */
void clearTabs() { void clearTabs() {
mTabLayout.removeAllTabs(); this.post(() -> {
if (mTabLayout != null) mTabLayout.removeAllTabs();
});
}
ViewPager.OnPageChangeListener getPageChangeListener() {
if (mPageChangeListener == null) {
mPageChangeListener = new TabLayout.TabLayoutOnPageChangeListener(mTabLayout);
}
return mPageChangeListener;
}
void setTabSelectionAdapter(TabLayout.OnTabSelectedListener tabSelectionCallbacks) {
mTabLayout.clearOnTabSelectedListeners();
mTabLayout.addOnTabSelectedListener(tabSelectionCallbacks);
}
void setActiveTabColor(Integer activeTab) {
for (int i = mTabLayout.getTabCount() - 1; i >= 0; i--) {
TabLayout.Tab t = mTabLayout.getTabAt(i);
if (t == null || t.getIcon() == null) continue;
if (activeTab == null || i != activeTab) {
t.getIcon().clearColorFilter();
} else {
t.getIcon().setColorFilter(Color.BLUE, PorterDuff.Mode.SRC_ATOP);
}
}
} }
// TODO(crbug/722897): Check to handle RTL. // TODO(crbug/722897): Check to handle RTL.
...@@ -125,6 +157,7 @@ class KeyboardAccessoryView extends LinearLayout { ...@@ -125,6 +157,7 @@ class KeyboardAccessoryView extends LinearLayout {
} }
private void show() { private void show() {
bringToFront(); // Needs to overlay every component and the bottom sheet - like a keyboard.
setVisibility(View.VISIBLE); setVisibility(View.VISIBLE);
announceForAccessibility(((ViewGroup) getParent()).getContentDescription()); announceForAccessibility(((ViewGroup) getParent()).getContentDescription());
} }
......
...@@ -89,7 +89,8 @@ class KeyboardAccessoryViewBinder ...@@ -89,7 +89,8 @@ class KeyboardAccessoryViewBinder
view.clearTabs(); view.clearTabs();
for (int i = 0; i < model.getItemCount(); ++i) { for (int i = 0; i < model.getItemCount(); ++i) {
Tab tab = model.get(i); Tab tab = model.get(i);
view.addTabAt(i, tab.getIcon(), tab.getContentDescription()); // Mutate tab icons so we can apply color filters.
view.addTabAt(i, tab.getIcon().mutate(), tab.getContentDescription());
} }
} }
} }
...@@ -123,6 +124,16 @@ class KeyboardAccessoryViewBinder ...@@ -123,6 +124,16 @@ class KeyboardAccessoryViewBinder
view.setVisible(model.isVisible()); view.setVisible(model.isVisible());
return; return;
} }
if (propertyKey == PropertyKey.ACTIVE_TAB) {
view.setActiveTabColor(model.activeTab());
return;
}
if (propertyKey == PropertyKey.TAB_SELECTION_CALLBACKS) {
// Don't add null as listener. It's a valid state but an invalid argument.
if (model.getTabSelectionCallbacks() == null) return;
view.setTabSelectionAdapter(model.getTabSelectionCallbacks());
return;
}
if (propertyKey == PropertyKey.SUGGESTIONS) { if (propertyKey == PropertyKey.SUGGESTIONS) {
view.updateSuggestions(model.getAutofillSuggestions()); view.updateSuggestions(model.getAutofillSuggestions());
return; return;
......
...@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory; ...@@ -7,6 +7,7 @@ package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.view.ViewStub; import android.view.ViewStub;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
/** /**
...@@ -18,8 +19,9 @@ import org.chromium.ui.base.WindowAndroid; ...@@ -18,8 +19,9 @@ import org.chromium.ui.base.WindowAndroid;
* fields. * fields.
*/ */
public class ManualFillingCoordinator { public class ManualFillingCoordinator {
private final KeyboardAccessoryCoordinator mKeyboardAccessory; private final ManualFillingMediator mMediator = new ManualFillingMediator();
private final AccessorySheetCoordinator mAccessorySheet; // 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.
/** /**
* Creates a the manual filling controller. * Creates a the manual filling controller.
...@@ -29,39 +31,40 @@ public class ManualFillingCoordinator { ...@@ -29,39 +31,40 @@ public class ManualFillingCoordinator {
*/ */
public ManualFillingCoordinator(WindowAndroid windowAndroid, ViewStub keyboardAccessoryStub, public ManualFillingCoordinator(WindowAndroid windowAndroid, ViewStub keyboardAccessoryStub,
ViewStub accessorySheetStub) { ViewStub accessorySheetStub) {
mKeyboardAccessory = new KeyboardAccessoryCoordinator(windowAndroid, keyboardAccessoryStub); assert windowAndroid.getActivity().get() != null;
mAccessorySheet = new AccessorySheetCoordinator(accessorySheetStub); KeyboardAccessoryCoordinator keyboardAccessory =
new KeyboardAccessoryCoordinator(windowAndroid, keyboardAccessoryStub, mMediator);
AccessorySheetCoordinator accessorySheet = new AccessorySheetCoordinator(
accessorySheetStub, keyboardAccessory::getPageChangeListener);
mMediator.initialize(keyboardAccessory, accessorySheet,
(ChromeActivity) windowAndroid.getActivity().get());
} }
/** /**
* Cleans up the manual UI by destroying the accessory bar and its bottom sheet. * Cleans up the manual UI by destroying the accessory bar and its bottom sheet.
*/ */
public void destroy() { public void destroy() {
mKeyboardAccessory.destroy(); mMediator.destroy();
} }
/** // TODO(fhorschig): Ideally, this isn't exposed. (Apply Hollywood principle to tabs).
* Links a tab to the manual UI by adding it to the held {@link AccessorySheetCoordinator} and void addTab(KeyboardAccessoryData.Tab tab) {
* the {@link KeyboardAccessoryCoordinator}. mMediator.addTab(tab);
* @param tab The tab component to be added.
*/
public void addTab(KeyboardAccessoryData.Tab tab) {
mKeyboardAccessory.addTab(tab);
mAccessorySheet.addTab(tab);
} }
public void removeTab(KeyboardAccessoryData.Tab tab) { // TODO(fhorschig): Ideally, this isn't exposed neither.
mKeyboardAccessory.removeTab(tab); void removeTab(KeyboardAccessoryData.Tab tab) {
mAccessorySheet.removeTab(tab); mMediator.removeTab(tab);
} }
// TODO(fhorschig): Should be @VisibleForTesting.
/** /**
* Allows access to the keyboard accessory. This can be used to explicitly modify the the bar of * Allows access to the keyboard accessory. This can be used to explicitly modify the the bar of
* the keyboard accessory (e.g. by providing suggestions or actions). * the keyboard accessory (e.g. by providing suggestions or actions).
* @return The coordinator of the Keyboard accessory component. * @return The coordinator of the Keyboard accessory component.
*/ */
public KeyboardAccessoryCoordinator getKeyboardAccessory() { public KeyboardAccessoryCoordinator getKeyboardAccessory() {
return mKeyboardAccessory; return mMediator.getKeyboardAccessory();
} }
/** /**
...@@ -69,7 +72,7 @@ public class ManualFillingCoordinator { ...@@ -69,7 +72,7 @@ public class ManualFillingCoordinator {
* @return The coordinator of the Accessory sheet component. * @return The coordinator of the Accessory sheet component.
*/ */
@VisibleForTesting @VisibleForTesting
public AccessorySheetCoordinator getAccessorySheetForTesting() { AccessorySheetCoordinator getAccessorySheetForTesting() {
return mAccessorySheet; return mMediator.getAccessorySheet();
} }
} }
\ 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 org.chromium.chrome.browser.ChromeActivity;
import org.chromium.ui.UiUtils;
/**
* 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 {
private KeyboardAccessoryCoordinator mKeyboardAccessory;
private AccessorySheetCoordinator mAccessorySheet;
private ChromeActivity mActivity; // Used to control the keyboard.
void initialize(KeyboardAccessoryCoordinator keyboardAccessory,
AccessorySheetCoordinator accessorySheet, ChromeActivity activity) {
mKeyboardAccessory = keyboardAccessory;
mAccessorySheet = accessorySheet;
mActivity = activity;
}
void destroy() {
// TODO(fhorschig): Remove all tabs. Destroy all providers.
// mPasswordAccessorySheet.destroy();
mKeyboardAccessory.destroy();
}
/**
* 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 removeTab(KeyboardAccessoryData.Tab tab) {
// TODO(fhorschig): Should remove Tabs per URL/chome tab.
mKeyboardAccessory.removeTab(tab);
mAccessorySheet.removeTab(tab);
}
@Override
public void onChangeAccessorySheet(int tabIndex) {
assert mActivity != null : "ManualFillingMediator needs initialization.";
mAccessorySheet.setActiveTab(tabIndex);
}
@Override
public void onOpenAccessorySheet() {
assert mActivity != null : "ManualFillingMediator needs initialization.";
UiUtils.hideKeyboard(mActivity.getCurrentFocus());
mAccessorySheet.show();
}
@Override
public void onCloseAccessorySheet() {
assert mActivity != null : "ManualFillingMediator needs initialization.";
mAccessorySheet.hide();
UiUtils.showKeyboard(mActivity.getCurrentFocus());
}
@Override
public void onCloseKeyboardAccessory() {
assert mActivity != null : "ManualFillingMediator needs initialization.";
mAccessorySheet.hide();
UiUtils.hideKeyboard(mActivity.getCurrentFocus());
}
// TODO(fhorschig): Should be @VisibleForTesting.
AccessorySheetCoordinator getAccessorySheet() {
return mAccessorySheet;
}
// TODO(fhorschig): Should be @VisibleForTesting.
KeyboardAccessoryCoordinator getKeyboardAccessory() {
return mKeyboardAccessory;
}
}
...@@ -25,6 +25,8 @@ class PasswordAccessoryBridge { ...@@ -25,6 +25,8 @@ class PasswordAccessoryBridge {
PasswordAccessorySheetCoordinator passwordAccessorySheet = PasswordAccessorySheetCoordinator passwordAccessorySheet =
new PasswordAccessorySheetCoordinator(activity); new PasswordAccessorySheetCoordinator(activity);
mTab = passwordAccessorySheet.createTab(); 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); passwordAccessorySheet.registerItemProvider(mItemProvider);
mManualFillingCoordinator.addTab(mTab); mManualFillingCoordinator.addTab(mTab);
} }
...@@ -43,7 +45,7 @@ class PasswordAccessoryBridge { ...@@ -43,7 +45,7 @@ class PasswordAccessoryBridge {
@CalledByNative @CalledByNative
private void destroy() { private void destroy() {
mNativeView = 0; mNativeView = 0;
mManualFillingCoordinator.removeTab(mTab); mManualFillingCoordinator.removeTab(mTab); // TODO(fhorschig): Should be "unregister".
} }
private Item[] convertToItems( private Item[] convertToItems(
......
...@@ -7,6 +7,8 @@ package org.chromium.chrome.browser.modelutil; ...@@ -7,6 +7,8 @@ package org.chromium.chrome.browser.modelutil;
import android.view.View; import android.view.View;
import android.view.ViewStub; import android.view.ViewStub;
import org.chromium.base.Callback;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/** /**
...@@ -41,7 +43,8 @@ public class LazyViewBinderAdapter<M extends PropertyObservable<K>, V extends Vi ...@@ -41,7 +43,8 @@ public class LazyViewBinderAdapter<M extends PropertyObservable<K>, V extends Vi
} }
@SuppressWarnings("unchecked") // The StubHolder always holds a V! @SuppressWarnings("unchecked") // The StubHolder always holds a V!
void inflateView() { void inflateView(ViewStub.OnInflateListener inflationCallback) {
mViewStub.setOnInflateListener(inflationCallback);
mView = (V) mViewStub.inflate(); mView = (V) mViewStub.inflate();
} }
} }
...@@ -78,24 +81,41 @@ public class LazyViewBinderAdapter<M extends PropertyObservable<K>, V extends Vi ...@@ -78,24 +81,41 @@ public class LazyViewBinderAdapter<M extends PropertyObservable<K>, V extends Vi
} }
private final SimpleViewBinder<M, V, K> mBinder; private final SimpleViewBinder<M, V, K> mBinder;
private final Callback<V> mPostInflationCallback;
/**
* Calls {LazyViewBinderAdapter#LazyViewBinderAdapter(SimpleViewBinder, Callback)} without a
* post-inflation callback
* @param binder The binder to be called with the fully inflated view.
* @see LazyViewBinderAdapter#LazyViewBinderAdapter(SimpleViewBinder, Callback)
*/
public LazyViewBinderAdapter(SimpleViewBinder<M, V, K> binder) {
this(binder, (view) -> {});
}
/** /**
* Creates the adapter with a {@link SimpleViewBinder} that will be used to determine when to * Creates the adapter with a {@link SimpleViewBinder} that will be used to determine when to
* inflate the {@link StubHolder} that is passed in by the {@link PropertyModelChangeProcessor}. * inflate the {@link StubHolder} that is passed in by the {@link PropertyModelChangeProcessor}.
* @param binder The binder to be called with the fully inflated view. * @param binder The binder to be called with the fully inflated view.
* @param postInflationCallback Callback called after inflation (before calling the ViewBinder).
*/ */
public LazyViewBinderAdapter(SimpleViewBinder<M, V, K> binder) { public LazyViewBinderAdapter(
SimpleViewBinder<M, V, K> binder, Callback<V> postInflationCallback) {
mBinder = binder; mBinder = binder;
mPostInflationCallback = postInflationCallback;
} }
@SuppressWarnings("unchecked") // The StubHolder always holds a V!
@Override @Override
public void bind(M model, StubHolder<V> stubHolder, K propertyKey) { public void bind(M model, StubHolder<V> stubHolder, K propertyKey) {
if (stubHolder.getView() == null) { if (stubHolder.getView() == null) {
if (propertyKey != mBinder.getVisibilityProperty() || !mBinder.isVisible(model)) { if (propertyKey != mBinder.getVisibilityProperty() || !mBinder.isVisible(model)) {
return; // Ignore model changes before the view is shown for the first time. return; // Ignore model changes before the view is shown for the first time.
} }
stubHolder.inflateView(); stubHolder.inflateView((viewStub, view) -> {
mBinder.onInitialInflation(model, stubHolder.getView()); mPostInflationCallback.onResult((V) view);
mBinder.onInitialInflation(model, (V) view);
});
} }
mBinder.bind(model, stubHolder.getView(), propertyKey); mBinder.bind(model, stubHolder.getView(), propertyKey);
} }
......
...@@ -112,6 +112,7 @@ chrome_java_sources = [ ...@@ -112,6 +112,7 @@ 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/ManualFillingMediator.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessoryBridge.java", "java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessoryBridge.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetCoordinator.java", "java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewBinder.java", "java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/PasswordAccessorySheetViewBinder.java",
...@@ -1621,6 +1622,8 @@ chrome_test_java_sources = [ ...@@ -1621,6 +1622,8 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/autofill/AutofillTest.java", "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/AutofillTestHelper.java",
"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/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/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/autofill/keyboard_accessory/PasswordAccessorySheetViewTest.java",
......
...@@ -61,8 +61,18 @@ public class AccessorySheetViewTest { ...@@ -61,8 +61,18 @@ public class AccessorySheetViewTest {
mStubHolder = new LazyViewBinderAdapter.StubHolder<>( mStubHolder = new LazyViewBinderAdapter.StubHolder<>(
mActivityTestRule.getActivity().findViewById(R.id.keyboard_accessory_sheet_stub)); mActivityTestRule.getActivity().findViewById(R.id.keyboard_accessory_sheet_stub));
mModel = new AccessorySheetModel(); mModel = new AccessorySheetModel();
mModel.addObserver(new PropertyModelChangeProcessor<>( mModel.addObserver(new PropertyModelChangeProcessor<>(mModel, mStubHolder,
mModel, mStubHolder, new LazyViewBinderAdapter<>(new AccessorySheetViewBinder()))); new LazyViewBinderAdapter<>(new AccessorySheetViewBinder(),
view -> view.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int i, float v, int i1) {}
@Override
public void onPageSelected(int i) {}
@Override
public void onPageScrollStateChanged(int i) {}
}))));
} }
@Test @Test
......
// 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.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
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.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import java.util.concurrent.TimeoutException;
/**
* 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 ManualFillingIntegrationTest {
@Rule
public final ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
new ChromeActivityTestRule<>(ChromeActivity.class);
private final ManualFillingTestHelper mHelper = new ManualFillingTestHelper(mActivityTestRule);
@Test
@SmallTest
public void testAccessoryIsAvailable() throws InterruptedException {
mHelper.loadTestPage(false);
mHelper.createTestTab();
Assert.assertNotNull("Controller for Manual filling should be available.",
mActivityTestRule.getActivity().getManualFillingController());
Assert.assertNotNull("Keyboard accessory should have an instance.",
mActivityTestRule.getActivity()
.getManualFillingController()
.getKeyboardAccessory());
Assert.assertNotNull("Accessory Sheet should have an instance.",
mActivityTestRule.getActivity()
.getManualFillingController()
.getAccessorySheetForTesting());
}
@Test
@SmallTest
public void testKeyboardAccessoryHiddenUntilKeyboardShows()
throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
mHelper.createTestTab();
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
onView(withId(R.id.keyboard_accessory)).check(doesNotExist());
mHelper.waitForKeyboard();
// Check that ONLY the accessory is there but the sheet is still hidden.
whenDisplayed(withId(R.id.keyboard_accessory));
onView(withId(R.id.keyboard_accessory_sheet)).check(doesNotExist());
}
@Test
@SmallTest
public void testKeyboardAccessoryDisappearsWithKeyboard()
throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
mHelper.createTestTab();
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
mHelper.waitForKeyboard();
whenDisplayed(withId(R.id.keyboard_accessory));
// Dismiss the keyboard to hide the accessory again.
mHelper.clickSubmit();
mHelper.waitForKeyboardToDisappear();
}
@Test
@SmallTest
public void testAccessorySheetHiddenUntilManuallyTriggered()
throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
mHelper.createTestTab();
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
mHelper.waitForKeyboard();
// Check that ONLY the accessory is there but the sheet is still hidden.
whenDisplayed(withId(R.id.keyboard_accessory));
onView(withId(R.id.keyboard_accessory_sheet)).check(doesNotExist());
// Trigger the sheet and wait for it to open and the keyboard to disappear.
onView(withId(R.id.tabs)).perform(selectTabAtPosition(0));
mHelper.waitForKeyboardToDisappear();
whenDisplayed(withId(R.id.keyboard_accessory_sheet));
}
@Test
@SmallTest
public void testHidingSheetBringsBackKeyboard() throws InterruptedException, TimeoutException {
mHelper.loadTestPage(false);
mHelper.createTestTab();
// Focus the field to bring up the accessory.
mHelper.clickPasswordField();
mHelper.waitForKeyboard();
// Click the tab to show the sheet and hide the keyboard.
whenDisplayed(withId(R.id.tabs)).perform(selectTabAtPosition(0));
mHelper.waitForKeyboardToDisappear();
whenDisplayed(withId(R.id.keyboard_accessory_sheet));
// Click the tab again to hide the sheet and show the keyboard.
onView(withId(R.id.tabs)).perform(selectTabAtPosition(0));
mHelper.waitForKeyboard();
onView(withId(R.id.keyboard_accessory)).check(matches(isDisplayed()));
mHelper.waitToBeHidden(withId(R.id.keyboard_accessory_sheet));
}
// TODO(fhorschig): Check that it overlays info bars.
}
// 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.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
import static org.hamcrest.core.AllOf.allOf;
import static org.chromium.chrome.test.util.ViewUtils.VIEW_GONE;
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 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;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.ViewInteraction;
import android.view.View;
import android.view.ViewGroup;
import org.hamcrest.Matcher;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.UiUtils;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* Helpers in this class simplify interactions with the Keyboard Accessory and the sheet below it.
*/
public class ManualFillingTestHelper {
private final ChromeActivityTestRule<ChromeActivity> mActivityTestRule;
private final AtomicReference<WebContents> mWebContentsRef = new AtomicReference<>();
ManualFillingTestHelper(ChromeActivityTestRule<ChromeActivity> activityTestRule) {
mActivityTestRule = activityTestRule;
}
public void loadTestPage(boolean isRtl) throws InterruptedException {
mActivityTestRule.startMainActivityWithURL(UrlUtils.encodeHtmlDataUri("<html"
+ (isRtl ? " dir=\"rtl\"" : "") + "><head>"
+ "<meta name=\"viewport\""
+ "content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" /></head>"
+ "<body><form method=\"POST\">"
+ "<input type=\"password\" id=\"password\"/><br>"
+ "<input type=\"text\" id=\"email\" autocomplete=\"email\" /><br>"
+ "<input type=\"submit\" id=\"submit\" />"
+ "</form></body></html>"));
setRtlForTesting(isRtl);
ThreadUtils.runOnUiThreadBlocking(
()
-> mWebContentsRef.set(
mActivityTestRule.getActivity().getActivityTab().getWebContents()));
DOMUtils.waitForNonZeroNodeBounds(mWebContentsRef.get(), "password");
}
public void waitForKeyboard() {
CriteriaHelper.pollUiThread(
()
-> UiUtils.isKeyboardShowing(InstrumentationRegistry.getContext(),
mActivityTestRule.getActivity().getCurrentFocus()));
}
public void waitForKeyboardToDisappear() {
CriteriaHelper.pollUiThread(
()
-> !UiUtils.isKeyboardShowing(InstrumentationRegistry.getContext(),
mActivityTestRule.getActivity().getCurrentFocus()));
}
public void clickPasswordField() throws TimeoutException, InterruptedException {
DOMUtils.clickNode(mWebContentsRef.get(), "password");
}
/**
* Although the submit button has no effect, it takes the focus from the input field and should
* hide the keyboard.
*/
public void clickSubmit() throws TimeoutException, InterruptedException {
DOMUtils.clickNode(mWebContentsRef.get(), "submit");
}
/**
* 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;
}
});
}
/**
* Use in a |onView().perform| action to select the tab at |tabIndex| for the found tab layout.
* @param tabIndex The index to be selected.
* @return The action executed by |perform|.
*/
static public ViewAction selectTabAtPosition(int tabIndex) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return allOf(isDisplayed(), isAssignableFrom(TabLayout.class));
}
@Override
public String getDescription() {
return "with tab at index " + tabIndex;
}
@Override
public void perform(UiController uiController, View view) {
TabLayout tabLayout = (TabLayout) view;
if (tabLayout.getTabAt(tabIndex) == null) {
throw new PerformException.Builder()
.withCause(new Throwable("No tab at index " + tabIndex))
.build();
}
tabLayout.getTabAt(tabIndex).select();
}
};
}
/**
* Use like {@link android.support.test.espresso.Espresso#onView}. It waits for a view matching
* the given |matcher| to be displayed and allows to chain checks/performs on the result.
* @param matcher The matcher matching exactly the view that is expected to be displayed.
* @return An interaction on the view matching |matcher.
*/
public static ViewInteraction whenDisplayed(Matcher<View> matcher) {
onView(isRoot()).check((r, e) -> waitForView((ViewGroup) r, allOf(matcher, isDisplayed())));
return onView(matcher);
}
public void waitToBeHidden(Matcher<View> matcher) {
onView(isRoot()).check((r, e) -> {
waitForView((ViewGroup) r, matcher, VIEW_INVISIBLE | VIEW_NULL | VIEW_GONE);
});
}
}
...@@ -11,6 +11,7 @@ import static org.junit.Assert.assertThat; ...@@ -11,6 +11,7 @@ import static org.junit.Assert.assertThat;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.test.filters.MediumTest; import android.support.test.filters.MediumTest;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.method.PasswordTransformationMethod; import android.text.method.PasswordTransformationMethod;
import android.widget.TextView; import android.widget.TextView;
...@@ -57,37 +58,35 @@ public class PasswordAccessorySheetViewTest { ...@@ -57,37 +58,35 @@ public class PasswordAccessorySheetViewTest {
*/ */
private void openLayoutInAccessorySheet( private void openLayoutInAccessorySheet(
@LayoutRes int layout, KeyboardAccessoryData.Tab.Listener listener) { @LayoutRes int layout, KeyboardAccessoryData.Tab.Listener listener) {
mActivityTestRule.getActivity() AccessorySheetCoordinator accessorySheet = new AccessorySheetCoordinator(
.getManualFillingController() mActivityTestRule.getActivity().findViewById(R.id.keyboard_accessory_sheet_stub),
.getAccessorySheetForTesting() () -> new ViewPager.OnPageChangeListener() {
.addTab(new KeyboardAccessoryData.Tab() { @Override
public void onPageScrolled(int i, float v, int i1) {}
@Override
public void onPageSelected(int i) {}
@Override
public void onPageScrollStateChanged(int i) {}
});
accessorySheet.addTab(new KeyboardAccessoryData.Tab() {
@Override @Override
public Drawable getIcon() { public Drawable getIcon() {
return null; return null;
} }
@Override @Override
public String getContentDescription() { public String getContentDescription() {
return null; return null;
} }
@Override @Override
public @LayoutRes int getTabLayout() { public @LayoutRes int getTabLayout() {
return layout; return layout;
} }
@Override @Override
public Listener getListener() { public Listener getListener() {
return listener; return listener;
} }
}); });
ThreadUtils.runOnUiThreadBlocking(() -> { ThreadUtils.runOnUiThreadBlocking(accessorySheet::show);
mActivityTestRule.getActivity()
.getManualFillingController()
.getAccessorySheetForTesting()
.getMediatorForTesting()
.show();
});
} }
@Before @Before
......
...@@ -58,7 +58,7 @@ public class AccessorySheetControllerTest { ...@@ -58,7 +58,7 @@ public class AccessorySheetControllerTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
when(mMockViewStub.inflate()).thenReturn(mMockView); when(mMockViewStub.inflate()).thenReturn(mMockView);
mCoordinator = new AccessorySheetCoordinator(mMockViewStub); mCoordinator = new AccessorySheetCoordinator(mMockViewStub, /*unused*/ () -> null);
mMediator = mCoordinator.getMediatorForTesting(); mMediator = mCoordinator.getMediatorForTesting();
mModel = mMediator.getModelForTesting(); mModel = mMediator.getModelForTesting();
} }
......
...@@ -15,7 +15,6 @@ import static org.mockito.Mockito.verify; ...@@ -15,7 +15,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.support.test.filters.SmallTest;
import android.view.ViewStub; import android.view.ViewStub;
import org.junit.Before; import org.junit.Before;
...@@ -26,7 +25,6 @@ import org.mockito.MockitoAnnotations; ...@@ -26,7 +25,6 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions; import org.chromium.chrome.browser.autofill.AutofillKeyboardSuggestions;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.PropertyProvider; import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.PropertyProvider;
...@@ -47,6 +45,8 @@ public class KeyboardAccessoryControllerTest { ...@@ -47,6 +45,8 @@ public class KeyboardAccessoryControllerTest {
@Mock @Mock
private ListObservable.ListObserver<Void> mMockActionListObserver; private ListObservable.ListObserver<Void> mMockActionListObserver;
@Mock @Mock
private KeyboardAccessoryCoordinator.VisibilityDelegate mMockVisibilityDelegate;
@Mock
private WindowAndroid mMockWindow; private WindowAndroid mMockWindow;
@Mock @Mock
private ViewStub mMockViewStub; private ViewStub mMockViewStub;
...@@ -75,14 +75,13 @@ public class KeyboardAccessoryControllerTest { ...@@ -75,14 +75,13 @@ public class KeyboardAccessoryControllerTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
when(mMockViewStub.inflate()).thenReturn(mMockView); when(mMockViewStub.inflate()).thenReturn(mMockView);
mCoordinator = new KeyboardAccessoryCoordinator(mMockWindow, mMockViewStub); mCoordinator = new KeyboardAccessoryCoordinator(
mMockWindow, mMockViewStub, mMockVisibilityDelegate);
mMediator = mCoordinator.getMediatorForTesting(); mMediator = mCoordinator.getMediatorForTesting();
mModel = mMediator.getModelForTesting(); mModel = mMediator.getModelForTesting();
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testCreatesValidSubComponents() { public void testCreatesValidSubComponents() {
assertThat(mCoordinator, is(notNullValue())); assertThat(mCoordinator, is(notNullValue()));
assertThat(mMediator, is(notNullValue())); assertThat(mMediator, is(notNullValue()));
...@@ -91,8 +90,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -91,8 +90,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testModelNotifiesVisibilityChangeOnShowAndHide() { public void testModelNotifiesVisibilityChangeOnShowAndHide() {
mModel.addObserver(mMockPropertyObserver); mModel.addObserver(mMockPropertyObserver);
...@@ -110,8 +107,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -110,8 +107,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testChangingTabsNotifiesTabObserver() { public void testChangingTabsNotifiesTabObserver() {
mModel.addTabListObserver(mMockTabListObserver); mModel.addTabListObserver(mMockTabListObserver);
...@@ -128,8 +123,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -128,8 +123,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testModelNotifiesAboutActionsChangedByProvider() { public void testModelNotifiesAboutActionsChangedByProvider() {
final PropertyProvider<Action> testProvider = new PropertyProvider<>(); final PropertyProvider<Action> testProvider = new PropertyProvider<>();
final FakeAction testAction = new FakeAction(); final FakeAction testAction = new FakeAction();
...@@ -160,8 +153,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -160,8 +153,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testModelDoesntNotifyUnchangedData() { public void testModelDoesntNotifyUnchangedData() {
mModel.addObserver(mMockPropertyObserver); mModel.addObserver(mMockPropertyObserver);
...@@ -179,8 +170,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -179,8 +170,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testIsVisibleWithSuggestionsBeforeKeyboardComesUp() { public void testIsVisibleWithSuggestionsBeforeKeyboardComesUp() {
// Without suggestions, the accessory should remain invisible - even if the keyboard shows. // Without suggestions, the accessory should remain invisible - even if the keyboard shows.
assertThat(mModel.getAutofillSuggestions(), is(nullValue())); assertThat(mModel.getAutofillSuggestions(), is(nullValue()));
...@@ -199,8 +188,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -199,8 +188,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testIsVisibleWithSuggestionsAfterKeyboardComesUp() { public void testIsVisibleWithSuggestionsAfterKeyboardComesUp() {
// Without any suggestions, the accessory should remain invisible. // Without any suggestions, the accessory should remain invisible.
assertThat(mModel.getAutofillSuggestions(), is(nullValue())); assertThat(mModel.getAutofillSuggestions(), is(nullValue()));
...@@ -216,8 +203,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -216,8 +203,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testIsVisibleWithActions() { public void testIsVisibleWithActions() {
// Without any actions, the accessory should remain invisible. // Without any actions, the accessory should remain invisible.
assertThat(mModel.getActionList().getItemCount(), is(0)); assertThat(mModel.getActionList().getItemCount(), is(0));
...@@ -230,8 +215,6 @@ public class KeyboardAccessoryControllerTest { ...@@ -230,8 +215,6 @@ public class KeyboardAccessoryControllerTest {
} }
@Test @Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testIsVisibleWithTabs() { public void testIsVisibleWithTabs() {
// Without any actions, the accessory should remain invisible. // Without any actions, the accessory should remain invisible.
assertThat(mModel.getActionList().getItemCount(), is(0)); assertThat(mModel.getActionList().getItemCount(), is(0));
......
...@@ -21,9 +21,12 @@ import org.mockito.MockitoAnnotations; ...@@ -21,9 +21,12 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.modelutil.ListObservable; import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
import java.lang.ref.WeakReference;
/** /**
* Controller tests for the root controller for interactions with the manual filling UI. * Controller tests for the root controller for interactions with the manual filling UI.
*/ */
...@@ -33,6 +36,8 @@ public class ManualFillingControllerTest { ...@@ -33,6 +36,8 @@ public class ManualFillingControllerTest {
@Mock @Mock
private WindowAndroid mMockWindow; private WindowAndroid mMockWindow;
@Mock @Mock
private ChromeActivity mMockActivity;
@Mock
private ViewStub mMockViewStub; private ViewStub mMockViewStub;
@Mock @Mock
private KeyboardAccessoryView mMockView; private KeyboardAccessoryView mMockView;
...@@ -47,6 +52,7 @@ public class ManualFillingControllerTest { ...@@ -47,6 +52,7 @@ public class ManualFillingControllerTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
when(mMockViewStub.inflate()).thenReturn(mMockView); when(mMockViewStub.inflate()).thenReturn(mMockView);
when(mMockWindow.getActivity()).thenReturn(new WeakReference<>(mMockActivity));
mController = new ManualFillingCoordinator(mMockWindow, mMockViewStub, mMockViewStub); mController = new ManualFillingCoordinator(mMockWindow, mMockViewStub, mMockViewStub);
} }
......
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