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