TouchInsetManager Introduction.

This changelist introduces TouchInsetManager to handle
touchable insets across different locations within the
same root view.

Bug: 214292772
Test: atest TouchInsetManagerTest
Change-Id: I9fa50d47440a130ffd8ed36fd9bc1e4f42ef303c
This commit is contained in:
Bryce Lee
2022-02-11 15:35:55 -08:00
parent 9492d508d8
commit 9bdbf29296
9 changed files with 450 additions and 83 deletions

View File

@@ -18,12 +18,9 @@ package com.android.systemui.dreams;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
@@ -62,43 +59,6 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
// Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
private final Handler mHandler;
// A hook into the internal inset calculation where we declare the overlays as the only
// touchable regions.
private final ViewTreeObserver.OnComputeInternalInsetsListener
mOnComputeInternalInsetsListener =
new ViewTreeObserver.OnComputeInternalInsetsListener() {
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(
ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
final Region region = new Region();
final Rect rect = new Rect();
final int childCount = mDreamOverlayContentView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = mDreamOverlayContentView.getChildAt(i);
if (mComplicationHostViewController.getView() == child) {
region.op(mComplicationHostViewController.getTouchRegions(),
Region.Op.UNION);
continue;
}
if (child.getGlobalVisibleRect(rect)) {
region.op(rect, Region.Op.UNION);
}
}
// Add the notifications drag area to the tap region (otherwise the
// notifications shade can't be dragged down).
if (mDreamOverlayContentView.getGlobalVisibleRect(rect)) {
rect.bottom = rect.top + mDreamOverlayNotificationsDragAreaHeight;
region.op(rect, Region.Op.UNION);
}
inoutInfo.touchableRegion.set(region);
}
};
@Inject
public DreamOverlayContainerViewController(
DreamOverlayContainerView containerView,
@@ -136,16 +96,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
@Override
protected void onViewAttached() {
mView.getViewTreeObserver()
.addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
}
@Override
protected void onViewDetached() {
mHandler.removeCallbacks(this::updateBurnInOffsets);
mView.getViewTreeObserver()
.removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
}
View getContainerView() {
@@ -162,6 +118,7 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
// so no translation occurs when the values don't change.
mView.setTranslationX(getBurnInOffset(mMaxBurnInOffset * 2, true)
- mMaxBurnInOffset);
mView.setTranslationY(getBurnInOffset(mMaxBurnInOffset * 2, false)
- mMaxBurnInOffset);

View File

@@ -24,6 +24,7 @@ import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.ViewController;
import java.lang.annotation.Retention;
@@ -48,6 +49,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
private static final int WIFI_STATUS_AVAILABLE = 2;
private final ConnectivityManager mConnectivityManager;
private final TouchInsetManager.TouchInsetSession mTouchInsetSession;
private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
.clearCapabilities()
@@ -77,9 +79,11 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
@Inject
public DreamOverlayStatusBarViewController(
DreamOverlayStatusBarView view,
ConnectivityManager connectivityManager) {
ConnectivityManager connectivityManager,
TouchInsetManager.TouchInsetSession touchInsetSession) {
super(view);
mConnectivityManager = connectivityManager;
mTouchInsetSession = touchInsetSession;
}
@Override
@@ -92,11 +96,13 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
onWifiAvailabilityChanged(
capabilities != null
&& capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI));
mTouchInsetSession.addViewToTracking(mView);
}
@Override
protected void onViewDetached() {
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
mTouchInsetSession.clear();
}
/**

View File

@@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Constraints;
import com.android.systemui.R;
import com.android.systemui.touch.TouchInsetManager;
import java.util.ArrayList;
import java.util.Collections;
@@ -52,6 +53,7 @@ public class ComplicationLayoutEngine {
private static class ViewEntry implements Comparable<ViewEntry> {
private final View mView;
private final ComplicationLayoutParams mLayoutParams;
private final TouchInsetManager.TouchInsetSession mTouchInsetSession;
private final Parent mParent;
@Complication.Category
private final int mCategory;
@@ -61,7 +63,8 @@ public class ComplicationLayoutEngine {
* Default constructor. {@link Parent} allows for the {@link ViewEntry}'s surrounding
* view hierarchy to be accessed without traversing the entire view tree.
*/
ViewEntry(View view, ComplicationLayoutParams layoutParams, int category, Parent parent,
ViewEntry(View view, ComplicationLayoutParams layoutParams,
TouchInsetManager.TouchInsetSession touchSession, int category, Parent parent,
int margin) {
mView = view;
// Views that are generated programmatically do not have a unique id assigned to them
@@ -70,9 +73,12 @@ public class ComplicationLayoutEngine {
// {@link Complication.ViewHolder} should not reference the root container by id.
mView.setId(View.generateViewId());
mLayoutParams = layoutParams;
mTouchInsetSession = touchSession;
mCategory = category;
mParent = parent;
mMargin = margin;
touchSession.addViewToTracking(mView);
}
/**
@@ -217,6 +223,7 @@ public class ComplicationLayoutEngine {
mParent.removeEntry(this);
((ViewGroup) mView.getParent()).removeView(mView);
mTouchInsetSession.removeViewFromTracking(mView);
}
@Override
@@ -242,15 +249,18 @@ public class ComplicationLayoutEngine {
*/
private static class Builder {
private final View mView;
private final TouchInsetManager.TouchInsetSession mTouchSession;
private final ComplicationLayoutParams mLayoutParams;
private final int mCategory;
private Parent mParent;
private int mMargin;
Builder(View view, ComplicationLayoutParams lp, @Complication.Category int category) {
Builder(View view, TouchInsetManager.TouchInsetSession touchSession,
ComplicationLayoutParams lp, @Complication.Category int category) {
mView = view;
mLayoutParams = lp;
mCategory = category;
mTouchSession = touchSession;
}
/**
@@ -291,7 +301,8 @@ public class ComplicationLayoutEngine {
* Builds and returns the resulting {@link ViewEntry}.
*/
ViewEntry build() {
return new ViewEntry(mView, mLayoutParams, mCategory, mParent, mMargin);
return new ViewEntry(mView, mLayoutParams, mTouchSession, mCategory, mParent,
mMargin);
}
}
@@ -442,13 +453,16 @@ public class ComplicationLayoutEngine {
private final int mMargin;
private final HashMap<ComplicationId, ViewEntry> mEntries = new HashMap<>();
private final HashMap<Integer, PositionGroup> mPositions = new HashMap<>();
private final TouchInsetManager.TouchInsetSession mSession;
/** */
@Inject
public ComplicationLayoutEngine(@Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout layout,
@Named(COMPLICATION_MARGIN) int margin) {
@Named(COMPLICATION_MARGIN) int margin,
TouchInsetManager.TouchInsetSession session) {
mLayout = layout;
mMargin = margin;
mSession = session;
}
/**
@@ -468,7 +482,7 @@ public class ComplicationLayoutEngine {
removeComplication(id);
}
final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, lp, category)
final ViewEntry.Builder entryBuilder = new ViewEntry.Builder(view, mSession, lp, category)
.setMargin(mMargin);
// Add position group if doesn't already exist

View File

@@ -29,6 +29,9 @@ import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayContainerView;
import com.android.systemui.dreams.DreamOverlayStatusBarView;
import com.android.systemui.touch.TouchInsetManager;
import java.util.concurrent.Executor;
import javax.inject.Named;
@@ -63,6 +66,21 @@ public abstract class DreamOverlayModule {
"R.id.dream_overlay_content must not be null");
}
/** */
@Provides
public static TouchInsetManager.TouchInsetSession providesTouchInsetSession(
TouchInsetManager manager) {
return manager.createSession();
}
/** */
@Provides
@DreamOverlayComponent.DreamOverlayScope
public static TouchInsetManager providesTouchInsetManager(@Main Executor executor,
DreamOverlayContainerView view) {
return new TouchInsetManager(executor, view);
}
/** */
@Provides
@DreamOverlayComponent.DreamOverlayScope

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.touch;
import android.graphics.Rect;
import android.graphics.Region;
import android.view.View;
import android.view.ViewRootImpl;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.Executor;
/**
* {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This
* is useful for passing through touch events for all but select areas.
*/
public class TouchInsetManager {
/**
* {@link TouchInsetSession} provides an individualized session with the
* {@link TouchInsetManager}, linking any action to the client.
*/
public static class TouchInsetSession {
private final TouchInsetManager mManager;
private final HashSet<View> mTrackedViews;
private final Executor mExecutor;
private final View.OnLayoutChangeListener mOnLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
-> updateTouchRegion();
/**
* Default constructor
* @param manager The parent {@link TouchInsetManager} which will be affected by actions on
* this session.
* @param rootView The parent of views that will be tracked.
* @param executor An executor for marshalling operations.
*/
TouchInsetSession(TouchInsetManager manager, Executor executor) {
mManager = manager;
mTrackedViews = new HashSet<>();
mExecutor = executor;
}
/**
* Adds a descendant of the root view to be tracked.
* @param view {@link View} to be tracked.
*/
public void addViewToTracking(View view) {
mExecutor.execute(() -> {
mTrackedViews.add(view);
view.addOnLayoutChangeListener(mOnLayoutChangeListener);
updateTouchRegion();
});
}
/**
* Removes a view from further tracking
* @param view {@link View} to be removed.
*/
public void removeViewFromTracking(View view) {
mExecutor.execute(() -> {
mTrackedViews.remove(view);
view.removeOnLayoutChangeListener(mOnLayoutChangeListener);
updateTouchRegion();
});
}
private void updateTouchRegion() {
final Region cumulativeRegion = Region.obtain();
mTrackedViews.stream().forEach(view -> {
final Rect boundaries = new Rect();
view.getBoundsOnScreen(boundaries);
cumulativeRegion.op(boundaries, Region.Op.UNION);
});
mManager.setTouchRegion(this, cumulativeRegion);
cumulativeRegion.recycle();
}
/**
* Removes all tracked views and updates insets accordingly.
*/
public void clear() {
mExecutor.execute(() -> {
mManager.clearRegion(this);
mTrackedViews.clear();
});
}
}
private final HashMap<TouchInsetSession, Region> mDefinedRegions = new HashMap<>();
private final Executor mExecutor;
private final View mRootView;
private final View.OnAttachStateChangeListener mAttachListener =
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
updateTouchInset();
}
@Override
public void onViewDetachedFromWindow(View v) {
}
};
/**
* Default constructor.
* @param executor An {@link Executor} to marshal all operations on.
* @param rootView The root {@link View} for all views in sessions.
*/
public TouchInsetManager(Executor executor, View rootView) {
mExecutor = executor;
mRootView = rootView;
mRootView.addOnAttachStateChangeListener(mAttachListener);
}
/**
* Creates a new associated session.
*/
public TouchInsetSession createSession() {
return new TouchInsetSession(this, mExecutor);
}
private void updateTouchInset() {
final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl();
if (viewRootImpl == null) {
return;
}
final Region aggregateRegion = Region.obtain();
for (Region region : mDefinedRegions.values()) {
aggregateRegion.op(region, Region.Op.UNION);
}
viewRootImpl.setTouchableRegion(aggregateRegion);
aggregateRegion.recycle();
}
protected void setTouchRegion(TouchInsetSession session, Region region) {
final Region introducedRegion = Region.obtain(region);
mExecutor.execute(() -> {
mDefinedRegions.put(session, introducedRegion);
updateTouchInset();
});
}
private void clearRegion(TouchInsetSession session) {
mExecutor.execute(() -> {
final Region storedRegion = mDefinedRegions.remove(session);
if (storedRegion != null) {
storedRegion.recycle();
}
updateTouchInset();
});
}
}

View File

@@ -17,7 +17,6 @@
package com.android.systemui.dreams;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
@@ -117,31 +116,6 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
DREAM_OVERLAY_NOTIFICATIONS_DRAG_AREA_HEIGHT);
}
@Test
public void testOnViewAttachedRegistersComputeInsetsListener() {
mController.onViewAttached();
verify(mViewTreeObserver).addOnComputeInternalInsetsListener(any());
}
@Test
public void testOnViewDetachedUnregistersComputeInsetsListener() {
mController.onViewDetached();
verify(mViewTreeObserver).removeOnComputeInternalInsetsListener(any());
}
@Test
public void testComputeInsetsListenerReturnsRegion() {
final ArgumentCaptor<ViewTreeObserver.OnComputeInternalInsetsListener>
computeInsetsListenerCapture =
ArgumentCaptor.forClass(ViewTreeObserver.OnComputeInternalInsetsListener.class);
mController.onViewAttached();
verify(mViewTreeObserver).addOnComputeInternalInsetsListener(
computeInsetsListenerCapture.capture());
final ViewTreeObserver.InternalInsetsInfo info = new ViewTreeObserver.InternalInsetsInfo();
computeInsetsListenerCapture.getValue().onComputeInternalInsets(info);
assertNotNull(info.touchableRegion);
}
@Test
public void testBurnInProtectionStartsWhenContentViewAttached() {
mController.onViewAttached();

View File

@@ -29,6 +29,7 @@ import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.touch.TouchInsetManager;
import org.junit.Before;
import org.junit.Test;
@@ -49,13 +50,16 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
NetworkCapabilities mNetworkCapabilities;
@Mock
Network mNetwork;
@Mock
TouchInsetManager.TouchInsetSession mTouchSession;
DreamOverlayStatusBarViewController mController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mController = new DreamOverlayStatusBarViewController(mView, mConnectivityManager);
mController = new DreamOverlayStatusBarViewController(mView, mConnectivityManager,
mTouchSession);
}
@Test

View File

@@ -29,6 +29,7 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.touch.TouchInsetManager;
import org.junit.Before;
import org.junit.Test;
@@ -46,6 +47,9 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
@Mock
ConstraintLayout mLayout;
@Mock
TouchInsetManager.TouchInsetSession mTouchSession;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
@@ -112,7 +116,8 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
Complication.CATEGORY_STANDARD,
mLayout);
final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, 0);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
addComplication(engine, firstViewInfo);
// Ensure the view is added to the top end corner
@@ -139,7 +144,8 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
Complication.CATEGORY_STANDARD,
mLayout);
final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, 0);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
addComplication(engine, firstViewInfo);
// Ensure the view is added to the top end corner
@@ -155,7 +161,8 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
*/
@Test
public void testDirectionLayout() {
final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, 0);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(
@@ -203,7 +210,8 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
*/
@Test
public void testPositionLayout() {
final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, 0);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(
@@ -290,7 +298,8 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
@Test
public void testMargin() {
final int margin = 5;
final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, margin);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, margin, mTouchSession);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(
@@ -364,7 +373,8 @@ public class ComplicationLayoutEngineTest extends SysuiTestCase {
*/
@Test
public void testRemoval() {
final ComplicationLayoutEngine engine = new ComplicationLayoutEngine(mLayout, 0);
final ComplicationLayoutEngine engine =
new ComplicationLayoutEngine(mLayout, 0, mTouchSession);
final ViewInfo firstViewInfo = new ViewInfo(
new ComplicationLayoutParams(

View File

@@ -0,0 +1,203 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.touch;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.view.ViewRootImpl;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class TouchInsetManagerTest extends SysuiTestCase {
@Mock
private View mRootView;
@Mock
private ViewRootImpl mRootViewImpl;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(mRootView.getViewRootImpl()).thenReturn(mRootViewImpl);
}
@Test
public void testRootViewOnAttachedHandling() {
// Create inset manager
final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
mRootView);
final ArgumentCaptor<View.OnAttachStateChangeListener> listener =
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
// Ensure manager has registered to listen to attached state of root view.
verify(mRootView).addOnAttachStateChangeListener(listener.capture());
// Trigger attachment and verify touchable region is set.
listener.getValue().onViewAttachedToWindow(mRootView);
verify(mRootViewImpl).setTouchableRegion(any());
}
@Test
public void testInsetRegionPropagation() {
// Create inset manager
final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
mRootView);
// Create session
final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
// Add a view to the session.
final Rect rect = new Rect(0, 0, 2, 2);
session.addViewToTracking(createView(rect));
mFakeExecutor.runAllReady();
// Check to see if view was properly accounted for.
final Region expectedRegion = Region.obtain();
expectedRegion.op(rect, Region.Op.UNION);
verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
}
@Test
public void testMultipleRegions() {
// Create inset manager
final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
mRootView);
// Create session
final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
// Add a view to the session.
final Rect firstBounds = new Rect(0, 0, 2, 2);
session.addViewToTracking(createView(firstBounds));
mFakeExecutor.runAllReady();
clearInvocations(mRootViewImpl);
// Create second session
final TouchInsetManager.TouchInsetSession secondSession = insetManager.createSession();
// Add a view to the second session.
final Rect secondBounds = new Rect(4, 4, 8, 10);
secondSession.addViewToTracking(createView(secondBounds));
mFakeExecutor.runAllReady();
// Check to see if all views and sessions was properly accounted for.
{
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstBounds, Region.Op.UNION);
expectedRegion.op(secondBounds, Region.Op.UNION);
verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
}
clearInvocations(mRootViewImpl);
// clear first session, ensure second session is still reflected.
session.clear();
mFakeExecutor.runAllReady();
{
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstBounds, Region.Op.UNION);
verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
}
}
@Test
public void testMultipleViews() {
// Create inset manager
final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
mRootView);
// Create session
final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
// Add a view to the session.
final Rect firstViewBounds = new Rect(0, 0, 2, 2);
session.addViewToTracking(createView(firstViewBounds));
// only capture second invocation.
mFakeExecutor.runAllReady();
clearInvocations(mRootViewImpl);
// Add a second view to the session
final Rect secondViewBounds = new Rect(4, 4, 9, 10);
final View secondView = createView(secondViewBounds);
session.addViewToTracking(secondView);
mFakeExecutor.runAllReady();
// Check to see if all views and sessions was properly accounted for.
{
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstViewBounds, Region.Op.UNION);
expectedRegion.op(secondViewBounds, Region.Op.UNION);
verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
}
// Remove second view.
session.removeViewFromTracking(secondView);
clearInvocations(mRootViewImpl);
mFakeExecutor.runAllReady();
// Ensure first view still reflected in touch region.
{
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstViewBounds, Region.Op.UNION);
verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
}
}
private View createView(Rect bounds) {
final Rect rect = new Rect(bounds);
final View view = Mockito.mock(View.class);
doAnswer(invocation -> {
((Rect) invocation.getArgument(0)).set(rect);
return null;
}).when(view).getBoundsOnScreen(any());
return view;
}
}