blob: c5d3d3d0ad25ef088c27a197dac8bd9119f019e3 [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 static org.chromium.chrome.browser.autofill.keyboard_accessory.AccessorySheetTrigger.MANUAL_CLOSE;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.design.widget.TabLayout;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryCoordinator.VisibilityDelegate;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This is the second part of the controller of the keyboard accessory component.
* It is responsible to update the {@link KeyboardAccessoryModel} based on Backend calls and notify
* the Backend if the {@link KeyboardAccessoryModel} changes.
* From the backend, it receives all actions that the accessory can perform (most prominently
* generating passwords) and lets the {@link KeyboardAccessoryModel} know of these actions and which
* callback to trigger when selecting them.
*/
class KeyboardAccessoryMediator
implements ListObservable.ListObserver<Void>,
PropertyObservable.PropertyObserver<KeyboardAccessoryModel.PropertyKey>,
KeyboardAccessoryData.Observer<KeyboardAccessoryData.Action>,
TabLayout.OnTabSelectedListener {
private final KeyboardAccessoryModel mModel;
private final VisibilityDelegate mVisibilityDelegate;
private boolean mShowIfNotEmpty;
KeyboardAccessoryMediator(KeyboardAccessoryModel model, VisibilityDelegate visibilityDelegate) {
mModel = model;
mVisibilityDelegate = visibilityDelegate;
// Add mediator as observer so it can use model changes as signal for accessory visibility.
mModel.addObserver(this);
mModel.getTabList().addObserver(this);
mModel.getActionList().addObserver(this);
mModel.setTabSelectionCallbacks(this);
}
@Override
public void onItemsAvailable(int typeId, KeyboardAccessoryData.Action[] actions) {
assert typeId != DEFAULT_TYPE : "Did not specify which Action type has been updated.";
// If there is a new list, retain all actions that are of a different type than the provided
// actions.
List<Action> retainedActions = new ArrayList<>();
for (Action a : mModel.getActionList()) {
if (a.getActionType() == typeId) continue;
retainedActions.add(a);
}
// Always append autofill suggestions to the very end.
int insertPos = typeId == AccessoryAction.AUTOFILL_SUGGESTION ? retainedActions.size() : 0;
retainedActions.addAll(insertPos, Arrays.asList(actions));
mModel.setActions(retainedActions.toArray(new Action[retainedActions.size()]));
}
void requestShowing() {
mShowIfNotEmpty = true;
updateVisibility();
}
void close() {
mShowIfNotEmpty = false;
updateVisibility();
}
void addTab(KeyboardAccessoryData.Tab tab) {
mModel.addTab(tab);
}
void removeTab(KeyboardAccessoryData.Tab tab) {
mModel.removeTab(tab);
}
void setTabs(KeyboardAccessoryData.Tab[] tabs) {
mModel.getTabList().set(tabs);
}
void dismiss() {
closeActiveTab();
updateVisibility();
}
void closeActiveTab() {
mModel.setActiveTab(null);
}
@VisibleForTesting
KeyboardAccessoryModel getModelForTesting() {
return mModel;
}
@Override
public void onItemRangeInserted(ListObservable source, int index, int count) {
assert source == mModel.getActionList() || source == mModel.getTabList();
updateVisibility();
}
@Override
public void onItemRangeRemoved(ListObservable source, int index, int count) {
assert source == mModel.getActionList() || source == mModel.getTabList();
updateVisibility();
}
@Override
public void onItemRangeChanged(
ListObservable source, int index, int count, @Nullable Void payload) {
assert source == mModel.getActionList() || source == mModel.getTabList();
assert payload == null;
updateVisibility();
}
@Override
public void onPropertyChanged(PropertyObservable<KeyboardAccessoryModel.PropertyKey> source,
@Nullable KeyboardAccessoryModel.PropertyKey propertyKey) {
// Update the visibility only if we haven't set it just now.
if (propertyKey == KeyboardAccessoryModel.PropertyKey.VISIBLE) {
// When the accessory just (dis)appeared, there should be no active tab.
closeActiveTab();
mVisibilityDelegate.onBottomControlSpaceChanged();
if (!mModel.isVisible()) {
// TODO(fhorschig|ioanap): Maybe the generation bridge should take care of that.
onItemsAvailable(AccessoryAction.GENERATE_PASSWORD_AUTOMATIC, new Action[0]);
}
return;
}
if (propertyKey == KeyboardAccessoryModel.PropertyKey.ACTIVE_TAB) {
Integer activeTab = mModel.activeTab();
if (activeTab == null) {
mVisibilityDelegate.onCloseAccessorySheet();
updateVisibility();
return;
}
mVisibilityDelegate.onChangeAccessorySheet(activeTab);
return;
}
if (propertyKey == KeyboardAccessoryModel.PropertyKey.BOTTOM_OFFSET
|| propertyKey == KeyboardAccessoryModel.PropertyKey.TAB_SELECTION_CALLBACKS) {
return;
}
assert false : "Every property update needs to be handled explicitly!";
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
mModel.setActiveTab(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
if (mModel.activeTab() == null) {
mModel.setActiveTab(tab.getPosition());
} else {
KeyboardAccessoryMetricsRecorder.recordSheetTrigger(
mModel.getTabList().get(mModel.activeTab()).getRecordingType(), MANUAL_CLOSE);
mVisibilityDelegate.onOpenKeyboard(); // This will close the active tab gently.
}
}
private boolean shouldShowAccessory() {
if (!mShowIfNotEmpty && mModel.activeTab() == null) return false;
return mModel.getActionList().size() > 0 || mModel.getTabList().size() > 0;
}
private void updateVisibility() {
mModel.setVisible(shouldShowAccessory());
}
public void setBottomOffset(@Px int bottomOffset) {
mModel.setBottomOffset(bottomOffset);
}
public boolean isShown() {
return mModel.isVisible();
}
public boolean hasActiveTab() {
return mModel.isVisible() && mModel.activeTab() != null;
}
}