1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.mockito.ArgumentMatchers.any;
23 import static org.mockito.Mockito.doReturn;
24 import static org.mockito.Mockito.never;
25 import static org.mockito.Mockito.spy;
26 import static org.mockito.Mockito.verify;
27 
28 import android.content.Context;
29 import android.graphics.Region;
30 import android.platform.test.annotations.Presubmit;
31 
32 import androidx.test.filters.SmallTest;
33 
34 import org.junit.Test;
35 
36 
37 /**
38  * Test basic functions of ViewGroup.
39  *
40  * Build/Install/Run:
41  *     atest FrameworksCoreTests:ViewGroupTest
42  */
43 @Presubmit
44 @SmallTest
45 public class ViewGroupTest {
46 
47     @Test
testDispatchMouseEventsUnderCursor()48     public void testDispatchMouseEventsUnderCursor() {
49         final Context context = getInstrumentation().getContext();
50         final TestView viewGroup = new TestView(context, 0 /* left */, 0 /* top */,
51                 200 /* right */, 200 /* bottom */);
52         final TestView viewA = spy(new TestView(context, 0 /* left */, 0 /* top */,
53                 100 /* right */, 200 /* bottom */));
54         final TestView viewB = spy(new TestView(context, 100 /* left */, 0 /* top */,
55                 200 /* right */, 200 /* bottom */));
56 
57         viewGroup.addView(viewA);
58         viewGroup.addView(viewB);
59 
60         // Make sure all of them handle touch events dispatched to them.
61         doReturn(true).when(viewA).dispatchTouchEvent(any());
62         doReturn(true).when(viewB).dispatchTouchEvent(any());
63 
64         MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[2];
65         properties[0] = new MotionEvent.PointerProperties();
66         properties[0].id = 0;
67         properties[0].toolType = MotionEvent.TOOL_TYPE_FINGER;
68         properties[1] = new MotionEvent.PointerProperties();
69         properties[1].id = 1;
70         properties[1].toolType = MotionEvent.TOOL_TYPE_FINGER;
71 
72         MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[2];
73         coords[0] = new MotionEvent.PointerCoords();
74         coords[0].x = 80;
75         coords[0].y = 100;
76         coords[1] = new MotionEvent.PointerCoords();
77         coords[1].x = 240;
78         coords[1].y = 100;
79 
80         MotionEvent event;
81         // Make sure the down event is active with a pointer which coordinate is different from the
82         // cursor position, which is the midpoint of all 2 pointers above.
83         event = MotionEvent.obtain(0 /* downTime */, 0 /* eventTime */, MotionEvent.ACTION_DOWN,
84                 2 /* pointerCount */, properties, coords, 0 /* metaState */, 0 /* buttonState */,
85                 0 /* xPrecision */, 0 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */,
86                 InputDevice.SOURCE_MOUSE, 0 /* flags */);
87         viewGroup.dispatchTouchEvent(event);
88         verify(viewB).dispatchTouchEvent(event);
89 
90         event = MotionEvent.obtain(0 /* downTime */, 0 /* eventTime */,
91                 MotionEvent.ACTION_POINTER_DOWN | (1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT),
92                 2 /* pointerCount */, properties, coords, 0 /* metaState */, 0 /* buttonState */,
93                 0 /* xPrecision */, 0 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */,
94                 InputDevice.SOURCE_MOUSE, 0 /* flags */);
95         viewGroup.dispatchTouchEvent(event);
96         verify(viewB).dispatchTouchEvent(event);
97 
98         verify(viewA, never()).dispatchTouchEvent(any());
99     }
100 
101     /**
102      * Test if {@link ViewGroup#subtractObscuredTouchableRegion} works as expected.
103      *
104      * The view hierarchy:
105      *   A---B---C
106      *    \   \
107      *     \   --D
108      *      \
109      *       E---F
110      *
111      * The layer and bounds of each view:
112      *   F -- (invisible)
113      *   E --
114      *   D ----
115      *   C ----------
116      *   B ------
117      *   A --------
118      */
119     @Test
testSubtractObscuredTouchableRegion()120     public void testSubtractObscuredTouchableRegion() {
121         final Context context = getInstrumentation().getContext();
122         final TestView viewA = new TestView(context, 8 /* right */);
123         final TestView viewB = new TestView(context, 6 /* right */);
124         final TestView viewC = new TestView(context, 10 /* right */);
125         final TestView viewD = new TestView(context, 4 /* right */);
126         final TestView viewE = new TestView(context, 2 /* right */);
127         final TestView viewF = new TestView(context, 2 /* right */);
128 
129         viewA.addView(viewB);
130         viewA.addView(viewE);
131         viewB.addView(viewC);
132         viewB.addView(viewD);
133         viewE.addView(viewF);
134 
135         viewF.setVisibility(View.INVISIBLE);
136 
137         final Region r = new Region();
138 
139         getUnobscuredTouchableRegion(r, viewA);
140         assertRegionContainPoint(1 /* x */, r, true /* contain */);
141         assertRegionContainPoint(3 /* x */, r, true /* contain */);
142         assertRegionContainPoint(5 /* x */, r, true /* contain */);
143         assertRegionContainPoint(7 /* x */, r, true /* contain */);
144         assertRegionContainPoint(9 /* x */, r, false /* contain */); // Outside of bounds
145 
146         getUnobscuredTouchableRegion(r, viewB);
147         assertRegionContainPoint(1 /* x */, r, false /* contain */); // Obscured by E
148         assertRegionContainPoint(3 /* x */, r, true /* contain */);
149         assertRegionContainPoint(5 /* x */, r, true /* contain */);
150         assertRegionContainPoint(7 /* x */, r, false /* contain */); // Outside of bounds
151 
152         getUnobscuredTouchableRegion(r, viewC);
153         assertRegionContainPoint(1 /* x */, r, false /* contain */); // Obscured by D and E
154         assertRegionContainPoint(3 /* x */, r, false /* contain */); // Obscured by D
155         assertRegionContainPoint(5 /* x */, r, true /* contain */);
156         assertRegionContainPoint(7 /* x */, r, false /* contain */); // Outside of parent bounds
157 
158         getUnobscuredTouchableRegion(r, viewD);
159         assertRegionContainPoint(1 /* x */, r, false /* contain */); // Obscured by E
160         assertRegionContainPoint(3 /* x */, r, true /* contain */);
161         assertRegionContainPoint(5 /* x */, r, false /* contain */); // Outside of bounds
162 
163         getUnobscuredTouchableRegion(r, viewE);
164         assertRegionContainPoint(1 /* x */, r, true /* contain */);
165         assertRegionContainPoint(3 /* x */, r, false /* contain */); // Outside of bounds
166     }
167 
getUnobscuredTouchableRegion(Region outRegion, View view)168     private static void getUnobscuredTouchableRegion(Region outRegion, View view) {
169         outRegion.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
170         final ViewParent parent = view.getParent();
171         if (parent != null) {
172             parent.subtractObscuredTouchableRegion(outRegion, view);
173         }
174     }
175 
assertRegionContainPoint(int x, Region region, boolean contain)176     private static void assertRegionContainPoint(int x, Region region, boolean contain) {
177         assertEquals(String.format("Touchable region must%s contain (%s, 0).",
178                 (contain ? "" : " not"), x), contain, region.contains(x, 0 /* y */));
179     }
180 
181     public static class TestView extends ViewGroup {
TestView(Context context, int right)182         TestView(Context context, int right) {
183             this(context, 0 /* left */, 0 /* top */, right, 1 /* bottom */);
184         }
185 
TestView(Context context, int left, int top, int right, int bottom)186         TestView(Context context, int left, int top, int right, int bottom) {
187             super(context);
188             setFrame(left, top, right, bottom);
189         }
190 
191         @Override
onLayout(boolean changed, int l, int t, int r, int b)192         protected void onLayout(boolean changed, int l, int t, int r, int b) {
193             // We don't layout this view.
194         }
195     }
196 }
197