Commit 0060e84c authored by Matt Jones's avatar Matt Jones Committed by Commit Bot

Add ApplicationViewportInsetProvider

This patch adds a uniform mechanism for controlling the viewport insets
for the application. The general approach is such that a feature using
the inset system doesn't have to worry about the specific tab it is
on, only whether there should currently be an inset. In more detail,
this patch does the following:

- The WindowAndroid owns the new ApplicationViewportInsetSupplier.
  - This object hosts basic logic to determine what the window inset
    should be given multiple providers. Currently this is just the
    max(...).
- Tabs register themselves as observers of the AVIS from the
  TabViewAndroidDelegate associated with their web contents.
- The TabViewAndroidDelegate hosts the actual inset functionality
  and reparenting and visibility logic. The tab is ignorant to this.
- The previously public insetViewportBottom has been made private and
  its only user converted to use the new system.

Bug: 1048183
Change-Id: I4e6b63e0e9faf29694dd8957bddd757db5b1f005
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2064597Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Commit-Queue: Matthew Jones <mdjones@chromium.org>
Cr-Commit-Position: refs/heads/master@{#749177}
parent a2177392
......@@ -205,6 +205,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/tab/TabBrowserControlsConstraintsHelperTest.java",
"junit/src/org/chromium/chrome/browser/tab/TabBrowserControlsOffsetHelperTest.java",
"junit/src/org/chromium/chrome/browser/tab/TabUnitTest.java",
"junit/src/org/chromium/chrome/browser/tab/TabViewAndroidDelegateTest.java",
"junit/src/org/chromium/chrome/browser/tabstate/TabStateUnitTest.java",
"junit/src/org/chromium/chrome/browser/tasks/EngagementTimeUtilTest.java",
"junit/src/org/chromium/chrome/browser/tasks/JourneyManagerTest.java",
......
......@@ -16,6 +16,7 @@ import android.widget.LinearLayout;
import android.widget.ScrollView;
import org.chromium.base.ObserverList;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.autofill_assistant.carousel.AssistantActionsCarouselCoordinator;
......@@ -30,12 +31,12 @@ import org.chromium.chrome.browser.autofill_assistant.infobox.AssistantInfoBoxCo
import org.chromium.chrome.browser.autofill_assistant.user_data.AssistantCollectUserDataCoordinator;
import org.chromium.chrome.browser.autofill_assistant.user_data.AssistantCollectUserDataModel;
import org.chromium.chrome.browser.compositor.CompositorViewResizer;
import org.chromium.chrome.browser.tab.TabViewAndroidDelegate;
import org.chromium.chrome.browser.ui.TabObscuringHandler;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetContent;
import org.chromium.chrome.browser.widget.bottomsheet.BottomSheetController;
import org.chromium.chrome.browser.widget.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
/**
* Coordinator responsible for the Autofill Assistant bottom bar.
......@@ -53,6 +54,7 @@ class AssistantBottomBarCoordinator
private final AssistantRootViewContainer mRootViewContainer;
@Nullable
private WebContents mWebContents;
private ApplicationViewportInsetSupplier mWindowApplicationInsetSupplier;
// Child coordinators.
private final AssistantHeaderCoordinator mHeaderCoordinator;
......@@ -60,6 +62,7 @@ class AssistantBottomBarCoordinator
private final AssistantFormCoordinator mFormCoordinator;
private final AssistantActionsCarouselCoordinator mActionsCoordinator;
private final AssistantPeekHeightCoordinator mPeekHeightCoordinator;
private final ObservableSupplierImpl<Integer> mInsetSupplier = new ObservableSupplierImpl<>();
private AssistantInfoBoxCoordinator mInfoBoxCoordinator;
private AssistantCollectUserDataCoordinator mPaymentRequestCoordinator;
private final AssistantGenericUiCoordinator mGenericUiCoordinator;
......@@ -84,6 +87,10 @@ class AssistantBottomBarCoordinator
mModel = model;
mBottomSheetController = controller;
mWindowApplicationInsetSupplier =
activity.getWindowAndroid().getApplicationBottomInsetProvider();
mWindowApplicationInsetSupplier.addSupplier(mInsetSupplier);
BottomSheetContent currentSheetContent = controller.getCurrentSheetContent();
if (currentSheetContent instanceof AssistantBottomSheetContent) {
mContent = (AssistantBottomSheetContent) currentSheetContent;
......@@ -262,6 +269,7 @@ class AssistantBottomBarCoordinator
*/
public void destroy() {
resetVisualViewportHeight();
mWindowApplicationInsetSupplier.removeSupplier(mInsetSupplier);
mInfoBoxCoordinator.destroy();
mInfoBoxCoordinator = null;
......@@ -396,10 +404,7 @@ class AssistantBottomBarCoordinator
}
mLastVisualViewportResizing = resizing;
TabViewAndroidDelegate chromeDelegate =
(TabViewAndroidDelegate) mWebContents.getViewAndroidDelegate();
assert chromeDelegate != null;
chromeDelegate.insetViewportBottom(resizing);
mInsetSupplier.set(resizing);
}
// Implementation of methods from AutofillAssistantSizeManager.
......
......@@ -6,6 +6,8 @@ package org.chromium.chrome.browser.tab;
import android.view.ViewGroup;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.content_public.browser.RenderWidgetHostView;
import org.chromium.ui.base.ViewAndroidDelegate;
......@@ -21,9 +23,41 @@ public class TabViewAndroidDelegate extends ViewAndroidDelegate {
*/
private int mApplicationViewportInsetBottomPx;
/** The inset supplier the observer is currently attached to. */
private ObservableSupplier<Integer> mCurrentInsetSupplier;
TabViewAndroidDelegate(Tab tab, ViewGroup containerView) {
super(containerView);
mTab = (TabImpl) tab;
Callback<Integer> insetObserver = (inset) -> updateInsetViewportBottom();
mCurrentInsetSupplier = tab.getWindowAndroid().getApplicationBottomInsetProvider();
mCurrentInsetSupplier.addObserver(insetObserver);
mTab.addObserver(new EmptyTabObserver() {
@Override
public void onActivityAttachmentChanged(Tab tab, boolean isAttached) {
if (isAttached) {
mCurrentInsetSupplier =
tab.getWindowAndroid().getApplicationBottomInsetProvider();
mCurrentInsetSupplier.addObserver(insetObserver);
} else {
mCurrentInsetSupplier.removeObserver(insetObserver);
mCurrentInsetSupplier = null;
updateInsetViewportBottom();
}
}
@Override
public void onShown(Tab tab, int type) {
updateInsetViewportBottom();
}
@Override
public void onHidden(Tab tab, int reason) {
updateInsetViewportBottom();
}
});
}
@Override
......@@ -45,12 +79,14 @@ public class TabViewAndroidDelegate extends ViewAndroidDelegate {
bottomControlsOffsetY, bottomControlsMinHeightOffsetY);
}
/**
* Sets the Visual Viewport bottom inset.
* @param viewportInsetBottomPx The bottom inset in pixels. Use {@code 0} for no inset.
*/
public void insetViewportBottom(int viewportInsetBottomPx) {
mApplicationViewportInsetBottomPx = viewportInsetBottomPx;
/** Sets the Visual Viewport bottom inset. */
private void updateInsetViewportBottom() {
int inset =
mTab.isHidden() || mCurrentInsetSupplier == null ? 0 : mCurrentInsetSupplier.get();
if (inset == mApplicationViewportInsetBottomPx) return;
mApplicationViewportInsetBottomPx = inset;
RenderWidgetHostView renderWidgetHostView = mTab.getWebContents().getRenderWidgetHostView();
if (renderWidgetHostView == null) return;
......
// Copyright 2020 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.tab;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.view.ViewGroup;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
import org.chromium.ui.base.WindowAndroid;
/** Unit tests for the TabViewAndroidDelegate. */
@RunWith(BaseRobolectricTestRunner.class)
public class TabViewAndroidDelegateTest {
private final ArgumentCaptor<TabObserver> mTabObserverCaptor =
ArgumentCaptor.forClass(TabObserver.class);
@Mock
private TabImpl mTab;
@Mock
private WebContents mWebContents;
@Mock
private WindowAndroid mWindowAndroid;
@Mock
private ViewGroup mViewContainer;
private ApplicationViewportInsetSupplier mApplicationInsetSupplier;
private ObservableSupplierImpl<Integer> mFeatureInsetSupplier;
private TabViewAndroidDelegate mViewAndroidDelegate;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mFeatureInsetSupplier = new ObservableSupplierImpl<>();
mApplicationInsetSupplier = ApplicationViewportInsetSupplier.createForTests();
mApplicationInsetSupplier.addSupplier(mFeatureInsetSupplier);
when(mWindowAndroid.getApplicationBottomInsetProvider())
.thenReturn(mApplicationInsetSupplier);
when(mTab.getWindowAndroid()).thenReturn(mWindowAndroid);
when(mTab.getWebContents()).thenReturn(mWebContents);
mViewAndroidDelegate = new TabViewAndroidDelegate(mTab, mViewContainer);
verify(mTab).addObserver(mTabObserverCaptor.capture());
}
@Test
public void testInset() {
mFeatureInsetSupplier.set(10);
assertEquals("The bottom inset for the tab should be non-zero.", 10,
mViewAndroidDelegate.getViewportInsetBottom());
}
@Test
public void testInset_afterHidden() {
mFeatureInsetSupplier.set(10);
when(mTab.isHidden()).thenReturn(true);
mTabObserverCaptor.getValue().onHidden(mTab, 0);
assertEquals("The bottom inset for the tab should be zero.", 0,
mViewAndroidDelegate.getViewportInsetBottom());
}
@Test
public void testInset_afterDetachAndAttach() {
mFeatureInsetSupplier.set(10);
assertEquals("The bottom inset for the tab should be non-zero.", 10,
mViewAndroidDelegate.getViewportInsetBottom());
mTabObserverCaptor.getValue().onActivityAttachmentChanged(mTab, false);
assertEquals("The bottom inset for the tab should be zero.", 0,
mViewAndroidDelegate.getViewportInsetBottom());
mTabObserverCaptor.getValue().onActivityAttachmentChanged(mTab, true);
assertEquals("The bottom inset for the tab should be non-zero.", 10,
mViewAndroidDelegate.getViewportInsetBottom());
}
}
......@@ -232,6 +232,7 @@ android_library("ui_full_java") {
"java/src/org/chromium/ui/base/ActivityWindowAndroid.java",
"java/src/org/chromium/ui/base/AndroidPermissionDelegate.java",
"java/src/org/chromium/ui/base/AndroidPermissionDelegateWithRequester.java",
"java/src/org/chromium/ui/base/ApplicationViewportInsetSupplier.java",
"java/src/org/chromium/ui/base/Clipboard.java",
"java/src/org/chromium/ui/base/DeviceFormFactor.java",
"java/src/org/chromium/ui/base/EventForwarder.java",
......@@ -377,6 +378,7 @@ junit_binary("ui_junit_tests") {
sources = [
"junit/src/org/chromium/ui/AsyncViewProviderTest.java",
"junit/src/org/chromium/ui/AsyncViewStubTest.java",
"junit/src/org/chromium/ui/base/ApplicationViewportInsetSupplierTest.java",
"junit/src/org/chromium/ui/base/ClipboardTest.java",
"junit/src/org/chromium/ui/base/EventOffsetHandlerTest.java",
"junit/src/org/chromium/ui/base/LocalizationUtilsTest.java",
......
// Copyright 2020 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.ui.base;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import java.util.HashSet;
import java.util.Set;
/**
* A class responsible for managing multiple users of window viewport insets. The class that
* actually updates the viewport insets listens to this class via {@link #addObserver(Callback)}.
* Other features can also listen to this class to know if something is obscuring part of the
* screen. Features that wish to update the viewport's insets can attach an inset supplier via
* {@link #addSupplier(ObservableSupplier)}. This class will use the largest inset value to
* adjust the viewport inset.
*
* In general:
* - Features that want to modify the inset should pass around the
* {@link ApplicationViewportInsetSupplier} object.
* - Features only interested in what the current inset is should pass around an
* {@link ObservableSupplier<Integer>} object.
*/
public class ApplicationViewportInsetSupplier extends ObservableSupplierImpl<Integer> {
/** The list of inset providers that this class manages. */
private final Set<ObservableSupplier<Integer>> mInsetSuppliers = new HashSet<>();
/** The observer that gets attached to all inset providers. */
private final Callback<Integer> mInsetSupplierObserver = (inset) -> computeInset();
/** Default constructor. */
ApplicationViewportInsetSupplier() {
super();
// Make sure this is initialized to 0 since "Integer" is an object and would be null
// otherwise.
super.set(0);
}
@VisibleForTesting
public static ApplicationViewportInsetSupplier createForTests() {
return new ApplicationViewportInsetSupplier();
}
/** Clean up observers and suppliers. */
public void destroy() {
for (ObservableSupplier<Integer> os : mInsetSuppliers) {
os.removeObserver(mInsetSupplierObserver);
}
mInsetSuppliers.clear();
}
/** Compute the new inset based on the current registered providers. */
private void computeInset() {
int currentInset = 0;
for (ObservableSupplier<Integer> os : mInsetSuppliers) {
// Similarly to the constructor, check that the Integer object isn't null as the
// supplier may not yet have supplied the initial value.
currentInset = Math.max(currentInset, os.get() == null ? 0 : os.get());
}
super.set(currentInset);
}
@Override
public void set(Integer value) {
throw new IllegalStateException(
"#set(...) should not be called directly on ApplicationViewportInsetSupplier.");
}
/** @param insetSupplier A supplier of bottom insets to be added. */
public void addSupplier(ObservableSupplier<Integer> insetSupplier) {
mInsetSuppliers.add(insetSupplier);
insetSupplier.addObserver(mInsetSupplierObserver);
}
/** @param insetSupplier A supplier of bottom insets to be removed. */
public void removeSupplier(ObservableSupplier<Integer> insetSupplier) {
mInsetSuppliers.remove(insetSupplier);
insetSupplier.removeObserver(mInsetSupplierObserver);
computeInset();
}
}
......@@ -121,6 +121,10 @@ public class WindowAndroid implements AndroidPermissionDelegate, DisplayAndroidO
// System accessibility service.
private final AccessibilityManager mAccessibilityManager;
/** A mechanism for observing and updating the application window's bottom inset. */
private ApplicationViewportInsetSupplier mApplicationBottomInsetProvider =
new ApplicationViewportInsetSupplier();
// Whether touch exploration is enabled.
private boolean mIsTouchExplorationEnabled;
......@@ -653,6 +657,7 @@ public class WindowAndroid implements AndroidPermissionDelegate, DisplayAndroidO
}
TouchlessEventHandler.removeCursorObserver(mCursorObserver);
mApplicationBottomInsetProvider.destroy();
}
/**
......@@ -733,6 +738,11 @@ public class WindowAndroid implements AndroidPermissionDelegate, DisplayAndroidO
return mKeyboardVisibilityDelegate;
}
/** @return A mechanism for updating and observing the bottom inset of the browser window. */
public ApplicationViewportInsetSupplier getApplicationBottomInsetProvider() {
return mApplicationBottomInsetProvider;
}
@VisibleForTesting
public void setKeyboardDelegate(KeyboardVisibilityDelegate keyboardDelegate) {
mKeyboardVisibilityDelegate = keyboardDelegate;
......
// Copyright 2020 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.ui.base;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRunner;
/** Unit tests for the ApplicationViewportInsetSupplier. */
@RunWith(BaseRobolectricTestRunner.class)
public class ApplicationViewportInsetSupplierTest {
/** A callback with the ability to get the last value pushed to it. */
private static class CapturingCallback<T> implements Callback<T> {
private T mValue;
@Override
public void onResult(T result) {
mValue = result;
}
public T getCapturedValue() {
return mValue;
}
}
private ApplicationViewportInsetSupplier mWindowApplicationInsetSupplier;
private ObservableSupplierImpl<Integer> mFeatureInsetSupplier;
private CapturingCallback<Integer> mWindowInsetObserver;
@Before
public void setUp() {
mWindowApplicationInsetSupplier = new ApplicationViewportInsetSupplier();
mFeatureInsetSupplier = new ObservableSupplierImpl<>();
mWindowInsetObserver = new CapturingCallback<>();
mWindowApplicationInsetSupplier.addSupplier(mFeatureInsetSupplier);
mWindowApplicationInsetSupplier.addObserver(mWindowInsetObserver);
}
@Test
public void testSupplierDidNotSetValue() {
assertEquals("Observed value from supplier is incorrect.", (Integer) 0,
mWindowApplicationInsetSupplier.get());
}
@Test
public void testSupplierTriggersObserver() {
mFeatureInsetSupplier.set(5);
assertEquals("Observed value from supplier is incorrect.", (Integer) 5,
mWindowInsetObserver.getCapturedValue());
}
@Test
public void testSupplierTriggersObserver_multipleSuppliers_2() {
ObservableSupplierImpl<Integer> secondSupplier = new ObservableSupplierImpl<>();
mWindowApplicationInsetSupplier.addSupplier(secondSupplier);
mFeatureInsetSupplier.set(5);
secondSupplier.set(10);
assertEquals("Observed value should be the max of the two supplied.", (Integer) 10,
mWindowInsetObserver.getCapturedValue());
}
@Test
public void testSupplierTriggersObserver_multipleSuppliers_3() {
ObservableSupplierImpl<Integer> secondSupplier = new ObservableSupplierImpl<>();
mWindowApplicationInsetSupplier.addSupplier(secondSupplier);
ObservableSupplierImpl<Integer> thirdSupplier = new ObservableSupplierImpl<>();
mWindowApplicationInsetSupplier.addSupplier(thirdSupplier);
mFeatureInsetSupplier.set(5);
secondSupplier.set(20);
thirdSupplier.set(10);
assertEquals("Observed value should be the max of the three supplied.", (Integer) 20,
mWindowInsetObserver.getCapturedValue());
}
@Test
public void testSupplierTriggersObserver_setBeforeAdded() {
ObservableSupplierImpl<Integer> supplier = new ObservableSupplierImpl<>();
supplier.set(20);
mWindowApplicationInsetSupplier.addSupplier(supplier);
assertEquals("The observer should have been triggered after the supplier was added.",
(Integer) 20, mWindowInsetObserver.getCapturedValue());
}
@Test
public void testSupplierRemoveTriggersEvent() {
ObservableSupplierImpl<Integer> secondSupplier = new ObservableSupplierImpl<>();
mWindowApplicationInsetSupplier.addSupplier(secondSupplier);
ObservableSupplierImpl<Integer> thirdSupplier = new ObservableSupplierImpl<>();
mWindowApplicationInsetSupplier.addSupplier(thirdSupplier);
mFeatureInsetSupplier.set(5);
secondSupplier.set(20);
thirdSupplier.set(10);
assertEquals("Observed value should be the max of the three supplied.", (Integer) 20,
mWindowInsetObserver.getCapturedValue());
mWindowApplicationInsetSupplier.removeSupplier(secondSupplier);
assertEquals("Observed value should be the max of the two remaining.", (Integer) 10,
mWindowInsetObserver.getCapturedValue());
}
@Test
public void testAllSuppliersRemoved() {
mFeatureInsetSupplier.set(5);
assertEquals("Observed value from supplier is incorrect.", (Integer) 5,
mWindowInsetObserver.getCapturedValue());
mWindowApplicationInsetSupplier.removeSupplier(mFeatureInsetSupplier);
assertEquals("Observed value should be 0 with no suppliers attached.", (Integer) 0,
mWindowInsetObserver.getCapturedValue());
}
}
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