1 /*
2  * Copyright (C) 2017 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.security.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 
21 import android.app.Activity;
22 import android.app.Instrumentation;
23 import android.app.UiAutomation;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.graphics.Color;
27 import android.graphics.Point;
28 import android.os.ParcelFileDescriptor;
29 import android.os.SystemClock;
30 import android.view.Gravity;
31 import android.view.InputDevice;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.WindowManager;
36 
37 import androidx.test.ext.junit.runners.AndroidJUnit4;
38 import androidx.test.filters.MediumTest;
39 import androidx.test.platform.app.InstrumentationRegistry;
40 import androidx.test.rule.ActivityTestRule;
41 
42 import com.android.compatibility.common.util.PollingCheck;
43 import com.android.compatibility.common.util.WidgetTestUtils;
44 
45 import org.junit.Before;
46 import org.junit.Rule;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 
50 import java.io.FileInputStream;
51 import java.io.InputStream;
52 import java.util.ArrayList;
53 import java.util.List;
54 import java.util.Scanner;
55 import java.util.concurrent.FutureTask;
56 import java.util.concurrent.TimeUnit;
57 
58 @MediumTest
59 @RunWith(AndroidJUnit4.class)
60 public class MotionEventTest {
61     private static final String TAG = "MotionEventTest";
62     private Activity mActivity;
63     private Instrumentation mInstrumentation;
64 
65     @Rule
66     public ActivityTestRule<MotionEventTestActivity> mActivityRule =
67             new ActivityTestRule<>(MotionEventTestActivity.class);
68 
69     @Before
setup()70     public void setup() {
71         mInstrumentation = InstrumentationRegistry.getInstrumentation();
72         mActivity = mActivityRule.getActivity();
73         PollingCheck.waitFor(mActivity::hasWindowFocus);
74     }
75 
76     /**
77      * Test for whether ACTION_OUTSIDE events contain information about whether touches are
78      * obscured.
79      *
80      * If ACTION_OUTSIDE_EVENTS contain information about whether the touch is obscured, then a
81      * pattern of invisible, untouchable, unfocusable application overlays can be placed across the
82      * screen to determine approximate locations of touch events without the user knowing.
83      */
84     @Test
testActionOutsideDoesNotContainedObscuredInformation()85     public void testActionOutsideDoesNotContainedObscuredInformation() throws Throwable {
86         enableAppOps();
87         final OnTouchListener listener = new OnTouchListener();
88         final WindowManager wm = mActivity.getSystemService(WindowManager.class);
89         WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(
90                 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
91                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
92                         WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
93                         WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
94 
95         FutureTask<View> addViewTask = new FutureTask<>(() -> {
96             final Point size = new Point();
97             wm.getDefaultDisplay().getSize(size);
98 
99             wmlp.width = size.x / 4;
100             wmlp.height = size.y / 4;
101             wmlp.gravity = Gravity.TOP | Gravity.LEFT;
102             wmlp.setTitle(mActivity.getPackageName());
103 
104             ViewGroup.LayoutParams vglp = new ViewGroup.LayoutParams(
105                     ViewGroup.LayoutParams.MATCH_PARENT,
106                     ViewGroup.LayoutParams.MATCH_PARENT);
107 
108             View v = new View(mActivity);
109             v.setOnTouchListener(listener);
110             v.setBackgroundColor(Color.GREEN);
111             v.setLayoutParams(vglp);
112             wm.addView(v, wmlp);
113 
114             wmlp.gravity = Gravity.TOP | Gravity.RIGHT;
115 
116             v = new View(mActivity);
117             v.setBackgroundColor(Color.BLUE);
118             v.setOnTouchListener(listener);
119             v.setLayoutParams(vglp);
120 
121             wm.addView(v, wmlp);
122             return v;
123         });
124         mActivity.runOnUiThread(addViewTask);
125         View view = addViewTask.get(5, TimeUnit.SECONDS);
126 
127         // Wait for a layout pass to be certain the view is on the screen
128         // before getting the location and injecting touches.
129         WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, view, null /*runnable*/,
130                 true /*forceLayout*/);
131 
132         // This ensures the window is visible, where the code above ensures
133         // the view is on screen.
134         mActivityRule.runOnUiThread(() -> {
135             // This will force WindowManager to relayout, ensuring the
136             // transaction to show the window are sent to the graphics code.
137             wm.updateViewLayout(view, wmlp);
138         });
139 
140         // Find the position inside the main activity and outside of the overlays.
141         FutureTask<Point> clickLocationTask = new FutureTask<>(() -> {
142             final int[] viewLocation = new int[2];
143             final View decorView = mActivity.getWindow().getDecorView();
144             decorView.getLocationOnScreen(viewLocation);
145             // Set y position to the center of the view, to make sure it is away from the status bar
146             return new Point(viewLocation[0], viewLocation[1] + decorView.getHeight() / 2);
147         });
148         mActivity.runOnUiThread(clickLocationTask);
149         Point viewLocation = clickLocationTask.get(5, TimeUnit.SECONDS);
150         injectTap(viewLocation.x, viewLocation.y);
151 
152         List<MotionEvent> outsideEvents = listener.getOutsideEvents();
153 
154         if (isRunningInVR()) {
155             // In VR mode we should be prevented from seeing any events.
156             assertEquals(0, outsideEvents.size());
157         } else {
158             assertEquals(2, outsideEvents.size());
159             for (MotionEvent e : outsideEvents) {
160                 assertEquals(0, e.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED);
161             }
162         }
163     }
164 
165 
enableAppOps()166     private void enableAppOps() {
167         StringBuilder cmd = new StringBuilder();
168         cmd.append("appops set ");
169         cmd.append(mInstrumentation.getContext().getPackageName());
170         cmd.append(" android:system_alert_window allow");
171         mInstrumentation.getUiAutomation().executeShellCommand(cmd.toString());
172 
173         StringBuilder query = new StringBuilder();
174         query.append("appops get ");
175         query.append(mInstrumentation.getContext().getPackageName());
176         query.append(" android:system_alert_window");
177         String queryStr = query.toString();
178 
179         String result;
180         do {
181             ParcelFileDescriptor pfd =
182                     mInstrumentation.getUiAutomation().executeShellCommand(queryStr);
183             InputStream inputStream = new FileInputStream(pfd.getFileDescriptor());
184             result = convertStreamToString(inputStream);
185         } while (result.contains("No operations"));
186     }
187 
convertStreamToString(InputStream is)188     private String convertStreamToString(InputStream is) {
189         try (Scanner s = new Scanner(is).useDelimiter("\\A")) {
190             return s.hasNext() ? s.next() : "";
191         }
192     }
193 
injectTap(int x, int y)194     private void injectTap(int x, int y) {
195         long downTime = SystemClock.uptimeMillis();
196         injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime);
197         injectEvent(MotionEvent.ACTION_UP, x, y, downTime);
198     }
199 
injectEvent(int action, int x, int y, long downTime)200     private void injectEvent(int action, int x, int y, long downTime) {
201         final UiAutomation automation = mInstrumentation.getUiAutomation();
202         final long eventTime = SystemClock.uptimeMillis();
203         MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0);
204         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
205         automation.injectInputEvent(event, true);
206         event.recycle();
207     }
208 
209     private static class OnTouchListener implements View.OnTouchListener {
210         private List<MotionEvent> mOutsideEvents;
211 
OnTouchListener()212         public OnTouchListener() {
213             mOutsideEvents = new ArrayList<>();
214         }
215 
onTouch(View v, MotionEvent e)216         public boolean onTouch(View v, MotionEvent e) {
217             if (e.getAction() == MotionEvent.ACTION_OUTSIDE) {
218                 mOutsideEvents.add(MotionEvent.obtain(e));
219             }
220             return true;
221         }
222 
getOutsideEvents()223         public List<MotionEvent> getOutsideEvents() {
224             return mOutsideEvents;
225         }
226     }
227 
isRunningInVR()228     private boolean isRunningInVR() {
229         final Context context = mInstrumentation.getTargetContext();
230         return (context.getResources().getConfiguration().uiMode &
231                 Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_VR_HEADSET;
232     }
233 }
234