| /* |
| * Copyright (C) 2017 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 android.security.cts; |
| |
| import static org.junit.Assert.assertEquals; |
| |
| import android.app.Activity; |
| import android.app.Instrumentation; |
| import android.app.UiAutomation; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.graphics.Color; |
| import android.graphics.Point; |
| import android.os.ParcelFileDescriptor; |
| import android.os.SystemClock; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.MediumTest; |
| import android.support.test.rule.ActivityTestRule; |
| import android.support.test.runner.AndroidJUnit4; |
| import android.view.Gravity; |
| import android.view.InputDevice; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.WindowManager; |
| |
| import com.android.compatibility.common.util.PollingCheck; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.FileInputStream; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Scanner; |
| import java.util.concurrent.FutureTask; |
| import java.util.concurrent.TimeUnit; |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4.class) |
| public class MotionEventTest { |
| private static final String TAG = "MotionEventTest"; |
| private Activity mActivity; |
| private Instrumentation mInstrumentation; |
| |
| @Rule |
| public ActivityTestRule<MotionEventTestActivity> mActivityRule = |
| new ActivityTestRule<>(MotionEventTestActivity.class); |
| |
| @Before |
| public void setup() { |
| mInstrumentation = InstrumentationRegistry.getInstrumentation(); |
| mActivity = mActivityRule.getActivity(); |
| PollingCheck.waitFor(mActivity::hasWindowFocus); |
| } |
| |
| /** |
| * Test for whether ACTION_OUTSIDE events contain information about whether touches are |
| * obscured. |
| * |
| * If ACTION_OUTSIDE_EVENTS contain information about whether the touch is obscured, then a |
| * pattern of invisible, untouchable, unfocusable application overlays can be placed across the |
| * screen to determine approximate locations of touch events without the user knowing. |
| */ |
| @Test |
| public void testActionOutsideDoesNotContainedObscuredInformation() throws Exception { |
| enableAppOps(); |
| final OnTouchListener listener = new OnTouchListener(); |
| final Point size = new Point(); |
| final View[] viewHolder = new View[1]; |
| mActivity.runOnUiThread(() -> { |
| final WindowManager wm = mActivity.getSystemService(WindowManager.class); |
| wm.getDefaultDisplay().getSize(size); |
| |
| WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams( |
| WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | |
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | |
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); |
| wmlp.width = size.x / 4; |
| wmlp.height = size.y / 4; |
| wmlp.gravity = Gravity.TOP | Gravity.LEFT; |
| wmlp.setTitle(mActivity.getPackageName()); |
| |
| ViewGroup.LayoutParams vglp = new ViewGroup.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT); |
| |
| View v = new View(mActivity); |
| v.setOnTouchListener(listener); |
| v.setBackgroundColor(Color.GREEN); |
| v.setLayoutParams(vglp); |
| wm.addView(v, wmlp); |
| |
| wmlp.gravity = Gravity.TOP | Gravity.RIGHT; |
| |
| v = new View(mActivity); |
| v.setBackgroundColor(Color.BLUE); |
| v.setOnTouchListener(listener); |
| v.setLayoutParams(vglp); |
| viewHolder[0] = v; |
| |
| wm.addView(v, wmlp); |
| }); |
| mInstrumentation.waitForIdleSync(); |
| |
| FutureTask<Point> task = new FutureTask<>(() -> { |
| final int[] viewLocation = new int[2]; |
| viewHolder[0].getLocationOnScreen(viewLocation); |
| return new Point(viewLocation[0], viewLocation[1]); |
| }); |
| mActivity.runOnUiThread(task); |
| Point viewLocation = task.get(5, TimeUnit.SECONDS); |
| injectTap(viewLocation.x, viewLocation.y); |
| |
| List<MotionEvent> outsideEvents = listener.getOutsideEvents(); |
| |
| if (isRunningInVR()) { |
| // In VR mode we should be prevented from seeing any events. |
| assertEquals(0, outsideEvents.size()); |
| } else { |
| assertEquals(2, outsideEvents.size()); |
| for (MotionEvent e : outsideEvents) { |
| assertEquals(0, e.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED); |
| } |
| } |
| } |
| |
| |
| private void enableAppOps() { |
| StringBuilder cmd = new StringBuilder(); |
| cmd.append("appops set "); |
| cmd.append(mInstrumentation.getContext().getPackageName()); |
| cmd.append(" android:system_alert_window allow"); |
| mInstrumentation.getUiAutomation().executeShellCommand(cmd.toString()); |
| |
| StringBuilder query = new StringBuilder(); |
| query.append("appops get "); |
| query.append(mInstrumentation.getContext().getPackageName()); |
| query.append(" android:system_alert_window"); |
| String queryStr = query.toString(); |
| |
| String result; |
| do { |
| ParcelFileDescriptor pfd = |
| mInstrumentation.getUiAutomation().executeShellCommand(queryStr); |
| InputStream inputStream = new FileInputStream(pfd.getFileDescriptor()); |
| result = convertStreamToString(inputStream); |
| } while (result.contains("No operations")); |
| } |
| |
| private String convertStreamToString(InputStream is) { |
| try (Scanner s = new Scanner(is).useDelimiter("\\A")) { |
| return s.hasNext() ? s.next() : ""; |
| } |
| } |
| |
| private void injectTap(int x, int y) { |
| long downTime = SystemClock.uptimeMillis(); |
| injectEvent(MotionEvent.ACTION_DOWN, x, y, downTime); |
| injectEvent(MotionEvent.ACTION_UP, x, y, downTime); |
| } |
| |
| private void injectEvent(int action, int x, int y, long downTime) { |
| final UiAutomation automation = mInstrumentation.getUiAutomation(); |
| final long eventTime = SystemClock.uptimeMillis(); |
| MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0); |
| event.setSource(InputDevice.SOURCE_TOUCHSCREEN); |
| automation.injectInputEvent(event, true); |
| event.recycle(); |
| } |
| |
| private static class OnTouchListener implements View.OnTouchListener { |
| private List<MotionEvent> mOutsideEvents; |
| |
| public OnTouchListener() { |
| mOutsideEvents = new ArrayList<>(); |
| } |
| |
| public boolean onTouch(View v, MotionEvent e) { |
| if (e.getAction() == MotionEvent.ACTION_OUTSIDE) { |
| mOutsideEvents.add(MotionEvent.obtain(e)); |
| } |
| return true; |
| } |
| |
| public List<MotionEvent> getOutsideEvents() { |
| return mOutsideEvents; |
| } |
| } |
| |
| private boolean isRunningInVR() { |
| final Context context = InstrumentationRegistry.getTargetContext(); |
| return (context.getResources().getConfiguration().uiMode & |
| Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_VR_HEADSET; |
| } |
| } |