blob: fa078c36047aca751592678959a9f38ad0f1dfc9 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.view.View;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Provider;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
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.DropdownPopupWindow;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.WindowAndroid;
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
extends EmptyTabObserver implements KeyboardAccessoryCoordinator.VisibilityDelegate {
private WindowAndroid mWindowAndroid;
private @Px int mPreviousControlHeight = 0;
private final WindowAndroid.KeyboardVisibilityListener mVisibilityListener =
this::onKeyboardVisibilityChanged;
/**
* Provides a cache for a given Provider which can repeat the last notification to all
* observers.
*/
private class ActionProviderCacheAdapter extends KeyboardAccessoryData.PropertyProvider<Action>
implements KeyboardAccessoryData.Observer<Action> {
private final Tab mTab;
private Action[] 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.
* @param defaultItems The items to be notified about if the Provider hasn't provided any.
*/
ActionProviderCacheAdapter(Tab tab, KeyboardAccessoryData.PropertyProvider<Action> provider,
Action[] defaultItems) {
super(provider.mType);
mTab = tab;
provider.addObserver(this);
mLastItems = defaultItems;
}
/**
* Calls {@link #onItemsAvailable} with the last used items again. If there haven't been
* any calls, call it with an empty list to avoid putting observers in an undefined state.
*/
void notifyAboutCachedItems() {
notifyObservers(mLastItems);
}
@Override
public void onItemsAvailable(int typeId, Action[] actions) {
mLastItems = actions;
// Update the contents immediately, if the adapter connects to an active element.
if (mTab == mActiveBrowserTab) notifyObservers(actions);
}
}
/**
* 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
ActionProviderCacheAdapter 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 DropdownPopupWindow mPopup;
private final SceneChangeObserver mTabSwitcherObserver = new SceneChangeObserver() {
@Override
public void onTabSelectionHinted(int tabId) {}
@Override
public void onSceneChange(Layout layout) {
// Includes events like side-swiping between tabs and triggering contextual search.
mKeyboardAccessory.dismiss();
}
};
private final TabObserver mTabObserver = new EmptyTabObserver() {
@Override
public void onHidden(Tab tab) {
mKeyboardAccessory.dismiss();
}
@Override
public void onDestroyed(Tab tab) {
mModel.remove(tab); // Clears tab if still present.
restoreCachedState(mActiveBrowserTab);
}
@Override
public void onEnterFullscreenMode(Tab tab, FullscreenOptions options) {
mKeyboardAccessory.dismiss();
}
};
void initialize(KeyboardAccessoryCoordinator keyboardAccessory,
AccessorySheetCoordinator accessorySheet, WindowAndroid windowAndroid) {
assert windowAndroid.getActivity().get() != null;
mWindowAndroid = windowAndroid;
mKeyboardAccessory = keyboardAccessory;
mAccessorySheet = accessorySheet;
mActivity = (ChromeActivity) windowAndroid.getActivity().get();
if (mActivity instanceof ChromeTabbedActivity) {
// This object typically lives as long as the layout manager, so there is no need to
// unsubscribe which would occasionally use an invalidated object.
((ChromeTabbedActivity) mActivity)
.getLayoutManager()
.addSceneChangeObserver(mTabSwitcherObserver);
}
windowAndroid.addKeyboardVisibilityListener(mVisibilityListener);
mTabModelObserver = new TabModelSelectorTabModelObserver(mActivity.getTabModelSelector()) {
@Override
public void didSelectTab(Tab tab, @TabModel.TabSelectionType int type, int lastId) {
mActiveBrowserTab = tab;
mPreviousControlHeight = mActivity.getFullscreenManager().getBottomControlsHeight();
restoreCachedState(tab);
}
@Override
public void willCloseTab(Tab tab, boolean animate) {
mModel.remove(tab);
restoreCachedState(mActiveBrowserTab);
}
};
Tab currentTab = mActivity.getTabModelSelector().getCurrentTab();
if (currentTab != null) {
mTabModelObserver.didSelectTab(
currentTab, TabModel.TabSelectionType.FROM_USER, Tab.INVALID_TAB_ID);
}
}
// TODO(fhorschig): Look for stronger signals than |keyboardVisibilityChanged|.
// This variable remembers the last state of |keyboardVisibilityChanged| which might not be
// sufficient for edge cases like hardware keyboards, floating keyboards, etc.
private void onKeyboardVisibilityChanged(boolean isShowing) {
if (isShowing) {
mKeyboardAccessory.requestShowing();
mActivity.getFullscreenManager().setBottomControlsHeight(calculateAccessoryBarHeight());
mKeyboardAccessory.closeActiveTab();
mKeyboardAccessory.setBottomOffset(0);
mAccessorySheet.hide();
} else {
mKeyboardAccessory.close();
if (mKeyboardAccessory.hasActiveTab()) {
mKeyboardAccessory.setBottomOffset(mAccessorySheet.getHeight());
mAccessorySheet.show();
onBottomControlSpaceChanged();
} else {
mActivity.getFullscreenManager().setBottomControlsHeight(mPreviousControlHeight);
}
}
}
void registerPasswordProvider(Provider<KeyboardAccessoryData.Item> itemProvider) {
assert getPasswordAccessorySheet() != null : "No password sheet available!";
getPasswordAccessorySheet().registerItemProvider(itemProvider);
}
void registerActionProvider(KeyboardAccessoryData.PropertyProvider<Action> actionProvider) {
ActionProviderCacheAdapter adapter =
new ActionProviderCacheAdapter(mActiveBrowserTab, actionProvider, new Action[0]);
mModel.get(mActiveBrowserTab).mActionsProvider = adapter;
getKeyboardAccessory().registerActionListProvider(adapter);
}
void destroy() {
if (mWindowAndroid != null) {
mWindowAndroid.removeKeyboardVisibilityListener(mVisibilityListener);
}
mTabModelObserver.destroy();
}
boolean handleBackPress() {
if (mAccessorySheet.isShown()) {
mKeyboardAccessory.dismiss();
return true;
}
return false;
}
void dismiss() {
mKeyboardAccessory.dismiss();
UiUtils.hideKeyboard(mActivity.getCurrentFocus());
}
void notifyPopupOpened(DropdownPopupWindow popup) {
mPopup = popup;
}
public void pause() {
mKeyboardAccessory.dismiss();
}
void resume() {
restoreCachedState(mActiveBrowserTab);
}
@Override
public void onChangeAccessorySheet(int tabIndex) {
assert mActivity != null : "ManualFillingMediator needs initialization.";
mAccessorySheet.setActiveTab(tabIndex);
if (mPopup != null && mPopup.isShowing()) mPopup.dismiss();
// If there is a keyboard, update the accessory sheet's height and hide the keyboard.
View focusedView = mActivity.getCurrentFocus();
if (focusedView != null) {
mAccessorySheet.setHeight(calculateAccessorySheetHeight(focusedView.getRootView()));
UiUtils.hideKeyboard(focusedView);
}
}
@Override
public void onCloseAccessorySheet() {
View focusedView = mActivity.getCurrentFocus();
if (focusedView == null || UiUtils.isKeyboardShowing(mActivity, focusedView)) {
return; // If the keyboard is showing or is starting to show, the sheet closes gently.
}
mActivity.getFullscreenManager().setBottomControlsHeight(mPreviousControlHeight);
mKeyboardAccessory.closeActiveTab();
mKeyboardAccessory.setBottomOffset(0);
mAccessorySheet.hide();
}
@Override
public void onOpenKeyboard() {
assert mActivity != null : "ManualFillingMediator needs initialization.";
mActivity.getFullscreenManager().setBottomControlsHeight(calculateAccessoryBarHeight());
UiUtils.showKeyboard(mActivity.getCurrentFocus());
}
@Override
public void onBottomControlSpaceChanged() {
@Px
int newControlsHeight = calculateAccessoryBarHeight();
if (mAccessorySheet.isShown()) {
newControlsHeight += mAccessorySheet.getHeight();
}
mActivity.getFullscreenManager().setBottomControlsHeight(newControlsHeight);
}
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) {
mKeyboardAccessory.dismiss();
clearTabs();
AccessoryState state = getOrCreateAccessoryState(browserTab);
if (state.mPasswordAccessorySheet != null) {
addTab(state.mPasswordAccessorySheet.getTab());
}
if (state.mActionsProvider != null) state.mActionsProvider.notifyAboutCachedItems();
}
private void clearTabs() {
mKeyboardAccessory.setTabs(new KeyboardAccessoryData.Tab[0]);
mAccessorySheet.setTabs(new KeyboardAccessoryData.Tab[0]);
}
private @Px int calculateAccessorySheetHeight(View rootView) {
int accessorySheetSuggestionHeight = mActivity.getResources().getDimensionPixelSize(
org.chromium.chrome.R.dimen.keyboard_accessory_suggestion_height);
// Ensure that the minimum height is always sufficient to display a suggestion.
return Math.max(accessorySheetSuggestionHeight,
UiUtils.calculateKeyboardHeight(mActivity, rootView));
}
private @Px int calculateAccessoryBarHeight() {
if (!mKeyboardAccessory.isShown()) return 0;
return mActivity.getResources().getDimensionPixelSize(
org.chromium.chrome.R.dimen.keyboard_accessory_suggestion_height);
}
@VisibleForTesting
void addTab(KeyboardAccessoryData.Tab tab) {
// TODO(fhorschig): This should add the tab only to the state. Sheet and accessory should be
// using a |set| method or even observe the state.
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;
}
}