Commit 260a5d3d authored by Friedrich Horschig's avatar Friedrich Horschig Committed by Commit Bot

Introduce controller for KeyboardAccessory

This is the first step of shaping the keyboard accessory into a
MVC-component as proposed in the Clank rearchitecture.
It only introduces the model and the controller of this component and
tests to verify the rudimentary design.

The wiring to the view (which technically exists as
KeyboardAccessoryView but needs to be restructured as well) will follow
in the next CL.

Bug: 828832
Change-Id: I791cf4a0a59671c5d674e50a6912d7242350290b
Reviewed-on: https://chromium-review.googlesource.com/1007084
Commit-Queue: Friedrich Horschig <fhorschig@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#550599}
parent ee78e496
// 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.base.VisibleForTesting;
/**
* Creates and owns all elements which are part of the keyboard accessory component.
* It's part of the controller but will mainly forward events (like adding a tab,
* or showing the accessory) to the {@link KeyboardAccessoryMediator}.
*/
public class KeyboardAccessoryCoordinator {
private final KeyboardAccessoryMediator mMediator =
new KeyboardAccessoryMediator(new KeyboardAccessoryModel());
@VisibleForTesting
KeyboardAccessoryMediator getMediatorForTesting() {
return mMediator;
}
public void hide() {
mMediator.hide();
}
public void show() {
mMediator.show();
}
public void addTab(KeyboardAccessoryData.Tab tab) {
mMediator.addTab(tab);
}
public void removeTab(KeyboardAccessoryData.Tab tab) {
mMediator.removeTab(tab);
}
/**
* Allows any {@link KeyboardAccessoryData.ActionListProvider} to communicate with the
* {@link KeyboardAccessoryMediator} of this component.
* @param provider The object providing action lists to observers in this component.
*/
public void registerActionListProvider(KeyboardAccessoryData.ActionListProvider provider) {
provider.addObserver(mMediator);
}
}
// 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;
/**
* Interfaces in this class are used to pass data into keyboard accessory component.
*/
public class KeyboardAccessoryData {
/**
* A provider notifies all registered {@link ActionListObserver} if the list of actions
* changes.
*/
public interface ActionListProvider {
/**
* Every observer added by this need to be notified whenever the list of action changes
* @param observer The observer to be notified.
*/
void addObserver(ActionListObserver observer);
}
/**
* An observer receives notifications from an {@link ActionListProvider} it is subscribed to.
*/
public interface ActionListObserver {
/**
* A provider calls this function with a list of actions that should be available in the
* keyboard accessory.
* @param actions The actions to be displayed in the Accessory. It's a native array as the
* provider is typically a bridge called via JNI which prefers native types.
*/
void onActionsAvailable(Action[] actions);
}
/**
* 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 {
// E.g. getIcon(), getDescription() and onTabSelected()
}
/**
* 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 {
// E.g. getCaption() or onActionSelected();
}
private KeyboardAccessoryData() {}
}
// 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.base.VisibleForTesting;
/**
* 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 KeyboardAccessoryData.ActionListObserver {
private final KeyboardAccessoryModel mModel;
KeyboardAccessoryMediator(KeyboardAccessoryModel keyboardAccessoryModel) {
mModel = keyboardAccessoryModel;
}
@Override
public void onActionsAvailable(KeyboardAccessoryData.Action[] actions) {
mModel.setActions(actions);
}
void hide() {
mModel.setVisible(false);
}
void show() {
mModel.setVisible(true);
}
void addTab(KeyboardAccessoryData.Tab tab) {
mModel.addTab(tab);
}
void removeTab(KeyboardAccessoryData.Tab tab) {
mModel.removeTab(tab);
}
@VisibleForTesting
KeyboardAccessoryModel getModelForTesting() {
return mModel;
}
}
// 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.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* As model of the keyboard accessory component, this class holds the data relevant to the visual
* state of the accessory.
* This includes the visibility of the accessory in general, any available tabs and actions.
* Whenever the state changes, it notifies its listeners - like the
* {@link KeyboardAccessoryMediator} or the ModelChangeProcessor.
*/
class KeyboardAccessoryModel extends PropertyObservable<KeyboardAccessoryModel.PropertyKey> {
/** Keys uniquely identifying model properties. */
static class PropertyKey {
static final PropertyKey VISIBLE = new PropertyKey();
private PropertyKey() {}
}
/** A {@link ListObservable} containing an {@link ArrayList} of Tabs or Actions. */
class SimpleListObservable<T> extends ListObservable {
private final List<T> mItems = new ArrayList<>();
public T get(int index) {
return mItems.get(index);
}
@Override
public int getItemCount() {
return mItems.size();
}
void add(T item) {
mItems.add(item);
notifyItemRangeInserted(mItems.size() - 1, 1);
}
void remove(T item) {
int position = mItems.indexOf(item);
if (position == -1) {
return;
}
mItems.remove(position);
notifyItemRangeRemoved(position, 1);
}
void set(T[] newItems) {
if (mItems.isEmpty()) {
if (newItems.length == 0) {
return; // Nothing to do, nothing changes.
}
mItems.addAll(Arrays.asList(newItems));
notifyItemRangeInserted(0, mItems.size());
return;
}
int oldSize = mItems.size();
mItems.clear();
if (newItems.length == 0) {
notifyItemRangeRemoved(0, oldSize);
return;
}
mItems.addAll(Arrays.asList(newItems));
notifyItemRangeChanged(0, Math.max(oldSize, mItems.size()), this);
}
}
private SimpleListObservable<KeyboardAccessoryData.Action> mActionListObservable;
private SimpleListObservable<KeyboardAccessoryData.Tab> mTabListObservable;
private boolean mVisible;
KeyboardAccessoryModel() {
mActionListObservable = new SimpleListObservable<>();
mTabListObservable = new SimpleListObservable<>();
}
void addActionListObserver(ListObservable.ListObserver observer) {
mActionListObservable.addObserver(observer);
}
void setActions(KeyboardAccessoryData.Action[] actions) {
mActionListObservable.set(actions);
}
SimpleListObservable<KeyboardAccessoryData.Action> getActionList() {
return mActionListObservable;
}
void addTabListObserver(ListObservable.ListObserver observer) {
mTabListObservable.addObserver(observer);
}
void addTab(KeyboardAccessoryData.Tab tab) {
mTabListObservable.add(tab);
}
void removeTab(KeyboardAccessoryData.Tab tab) {
mTabListObservable.remove(tab);
}
SimpleListObservable<KeyboardAccessoryData.Tab> getTabList() {
return mTabListObservable;
}
void setVisible(boolean visible) {
mVisible = visible;
notifyPropertyChanged(PropertyKey.VISIBLE);
}
boolean isVisible() {
return mVisible;
}
}
......@@ -97,6 +97,10 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/autofill/PasswordGenerationPopupBridge.java",
"java/src/org/chromium/chrome/browser/autofill/PersonalDataManager.java",
"java/src/org/chromium/chrome/browser/autofill/PhoneNumberUtil.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryCoordinator.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryData.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryMediator.java",
"java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryModel.java",
"java/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTask.java",
"java/src/org/chromium/chrome/browser/banners/AppBannerManager.java",
"java/src/org/chromium/chrome/browser/banners/AppBannerUiDelegateAndroid.java",
......@@ -1952,6 +1956,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/DisableHistogramsRule.java",
"junit/src/org/chromium/chrome/browser/ShortcutHelperTest.java",
"junit/src/org/chromium/chrome/browser/SSLClientCertificateRequestTest.java",
"junit/src/org/chromium/chrome/browser/autofill/keyboard_accessory/KeyboardAccessoryControllerTest.java",
"junit/src/org/chromium/chrome/browser/background_task_scheduler/NativeBackgroundTaskTest.java",
"junit/src/org/chromium/chrome/browser/browseractions/BrowserActionsIntentTest.java",
"junit/src/org/chromium/chrome/browser/compositor/animation/CompositorAnimatorTest.java",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.support.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.modelutil.ListObservable;
import org.chromium.chrome.browser.modelutil.PropertyObservable.PropertyObserver;
import java.util.ArrayList;
import java.util.List;
/**
* Controller tests for the keyboard accessory component.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class KeyboardAccessoryControllerTest {
@Mock
PropertyObserver<KeyboardAccessoryModel.PropertyKey> mMockPropertyObserver;
@Mock
ListObservable.ListObserver mMockTabListObserver;
@Mock
ListObservable.ListObserver mMockActionListObserver;
private class TestActionListProvider implements KeyboardAccessoryData.ActionListProvider {
private final List<KeyboardAccessoryData.ActionListObserver> mObservers = new ArrayList<>();
@Override
public void addObserver(KeyboardAccessoryData.ActionListObserver observer) {
mObservers.add(observer);
}
public void sendActionsToReceivers(KeyboardAccessoryData.Action[] actions) {
for (KeyboardAccessoryData.ActionListObserver observer : mObservers) {
observer.onActionsAvailable(actions);
}
}
}
private static class FakeTab implements KeyboardAccessoryData.Tab {}
private static class FakeAction implements KeyboardAccessoryData.Action {}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testCreatesValidSubComponents() {
final KeyboardAccessoryCoordinator coordinator = new KeyboardAccessoryCoordinator();
final KeyboardAccessoryMediator mediator = coordinator.getMediatorForTesting();
assertThat(mediator, is(notNullValue()));
final KeyboardAccessoryModel model = mediator.getModelForTesting();
assertThat(model, is(notNullValue()));
}
@Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testModelNotifiesVisibilityChangeOnShowAndHide() {
final KeyboardAccessoryCoordinator coordinator = new KeyboardAccessoryCoordinator();
final KeyboardAccessoryModel model =
coordinator.getMediatorForTesting().getModelForTesting();
model.addObserver(mMockPropertyObserver);
// Calling show on the coordinator should make model propagate that it's visible.
coordinator.show();
verify(mMockPropertyObserver)
.onPropertyChanged(model, KeyboardAccessoryModel.PropertyKey.VISIBLE);
assertThat(model.isVisible(), is(true));
// Calling hide on the coordinator should make model propagate that it's invisible.
coordinator.hide();
verify(mMockPropertyObserver, times(2))
.onPropertyChanged(model, KeyboardAccessoryModel.PropertyKey.VISIBLE);
assertThat(model.isVisible(), is(false));
}
@Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testChangingTabsNotifiesTabObserver() {
final KeyboardAccessoryCoordinator coordinator = new KeyboardAccessoryCoordinator();
final KeyboardAccessoryModel model =
coordinator.getMediatorForTesting().getModelForTesting();
final FakeTab testTab = new FakeTab();
model.addTabListObserver(mMockTabListObserver);
// Calling addTab on the coordinator should make model propagate that it has a new tab.
coordinator.addTab(testTab);
verify(mMockTabListObserver).onItemRangeInserted(model.getTabList(), 0, 1);
assertThat(model.getTabList().getItemCount(), is(1));
assertThat(model.getTabList().get(0), is(equalTo(testTab)));
// Calling hide on the coordinator should make model propagate that it's invisible.
coordinator.removeTab(testTab);
verify(mMockTabListObserver).onItemRangeRemoved(model.getTabList(), 0, 1);
assertThat(model.getTabList().getItemCount(), is(0));
}
@Test
@SmallTest
@Feature({"keyboard-accessory"})
public void testModelNotifiesAboutActionsChangedByProvider() {
final KeyboardAccessoryCoordinator coordinator = new KeyboardAccessoryCoordinator();
final KeyboardAccessoryModel model =
coordinator.getMediatorForTesting().getModelForTesting();
final TestActionListProvider testProvider = new TestActionListProvider();
final FakeAction testAction = new FakeAction();
model.addActionListObserver(mMockActionListObserver);
coordinator.registerActionListProvider(testProvider);
// If the mediator receives an initial set of actions, the model should report an insertion.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {testAction});
verify(mMockActionListObserver).onItemRangeInserted(model.getActionList(), 0, 1);
assertThat(model.getActionList().getItemCount(), is(1));
assertThat(model.getActionList().get(0), is(equalTo(testAction)));
// If the mediator receives a new set of actions, the model should report a change.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {testAction});
verify(mMockActionListObserver)
.onItemRangeChanged(model.getActionList(), 0, 1, model.getActionList());
assertThat(model.getActionList().getItemCount(), is(1));
assertThat(model.getActionList().get(0), is(equalTo(testAction)));
// If the mediator receives an empty set of actions, the model should report a deletion.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {});
verify(mMockActionListObserver).onItemRangeRemoved(model.getActionList(), 0, 1);
assertThat(model.getActionList().getItemCount(), is(0));
// There should be no notification if no actions are reported repeatedly.
testProvider.sendActionsToReceivers(new KeyboardAccessoryData.Action[] {});
verifyNoMoreInteractions(mMockActionListObserver);
}
}
\ No newline at end of file
file://chrome/android/java/src/org/chromium/chrome/browser/autofill/keyboard_accessory/OWNERS
# COMPONENT: UI>Browser>Autofill
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