blob: 0ceee07f446ff95193228266282fcfd880cd4599 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.graphics.Rect
import android.os.SystemClock
import android.platform.test.annotations.AsbSecurityTest
import android.view.Gravity
import android.view.InputDevice
import android.view.MotionEvent
import android.view.SurfaceControlViewHost
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.android.compatibility.common.util.PollingCheck
import com.android.sts.common.util.StsExtraBusinessLogicTestCase
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.CountDownLatch
import java.util.concurrent.LinkedBlockingQueue
import java.util.Queue
val FLAG_SLIPPERY = 0x20000000 // android.view.WindowManager.LayoutParams.FLAG_SLIPPERY
private fun getViewCenterOnScreen(v: View): Pair<Float, Float> {
val location = IntArray(2)
v.getLocationOnScreen(location)
val x = location[0] + v.width / 2f
val y = location[1] + v.height / 2f
return Pair(x, y)
}
private fun assertAction(action: Int, event: MotionEvent?) {
if (event == null) {
fail("Expected ${MotionEvent.actionToString(action)}, but got a null event instead")
return
}
assertEquals("Expected ${MotionEvent.actionToString(action)}, but received " +
"${MotionEvent.actionToString(event.action)}", action, event.action)
}
private class SurfaceCreatedCallback(created: CountDownLatch) : SurfaceHolder.Callback {
private val surfaceCreated = created
override fun surfaceCreated(holder: SurfaceHolder) {
surfaceCreated.countDown()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
}
@MediumTest
@RunWith(AndroidJUnit4::class)
/**
* Non-system windows cannot use FLAG_SLIPPERY. In this test, we test that if a window specifies
* this flag, then the flag is dropped.
* It's not sufficient to simply check that the flag is dropped, so we test the actual behaviour of
* the windows.
* There are 2 windows in this test:
* 1) The bottom activity, which is full screen. It's called 'SlipperyEnterBottomActivity' because
* the touch will enter this activity from the slippery window
* 2) The top window, 'topView' (or 'surfaceView'/'embeddedView'). This is the window that specifies
* the FLAG_SLIPPERY in its layout params. We could also call it 'SlipperyExit' window, because
* the touch will exit from this window if the slippery behaviour is enabled.
*
* The test does the following:
* 1) Inject DOWN event to the slippery (top) window. When the top window receives the DOWN event,
* it moves itself out of the way, so that the user effectively sees that the finger is over the
* bottom activity instead.
* 2) Inject MOVE event. If the top window were indeed slippery, this MOVE event would end up going
* to the bottom activity, and would become a DOWN event instead (since until that point, activity
* does not have an active touch stream). However, since we are not allowing this top window to be
* slippery, the MOVE event should just continue to be delivered to the top window.
* In this test, we are checking that the top window received the MOVE event, and that the bottom
* window did not receive any events.
*
* There are several ways to specify FLAG_SLIPPERY on a window (or a generic entity that receives
* touch). These are:
* 1) WindowManagerService::addWindow
* 2) WindowManagerService::relayoutWindow
* 3) WindowManagerService::grantInputChannel
*
* So we should test all 3 of these approaches. The first 2 are similar, so they share the same
* test code. The third approach requires adding an embedded window, and the code for that test was
* forked to avoid excessive branching.
*/
class FlagSlipperyTest : StsExtraBusinessLogicTestCase {
private lateinit var scenario: ActivityScenario<SlipperyEnterBottomActivity>
private lateinit var windowManager: WindowManager
private val nonSlipperyLayoutParams = WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY,
FLAG_NOT_TOUCH_MODAL)
private val slipperyLayoutParams = WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY,
FLAG_NOT_TOUCH_MODAL or FLAG_SLIPPERY)
private val layoutCompleted = AtomicBoolean(false)
private val eventsForTopWindow: Queue<MotionEvent> = LinkedBlockingQueue()
private var viewToRemove: View? = null
@get:Rule
val rule = ActivityScenarioRule<SlipperyEnterBottomActivity>(
SlipperyEnterBottomActivity::class.java)
constructor() : super()
@Before
fun setup() {
scenario = rule.getScenario()
windowManager = getInstrumentation().getTargetContext().getSystemService<WindowManager>(
WindowManager::class.java)
setDimensionsToQuarterScreen()
waitForWindowFocusOnBottomActivity()
}
@After
fun tearDown() {
eventsForTopWindow.clear()
if (viewToRemove != null) {
scenario.onActivity {
windowManager.removeViewImmediate(viewToRemove)
}
viewToRemove = null
}
}
// ========================== Regular window tests =============================================
private fun addWindow(slipperyWhenAdded: Boolean): View {
val view = View(getInstrumentation().targetContext)
scenario.onActivity {
view.setOnTouchListener(OnTouchListener(view))
view.setBackgroundColor(android.graphics.Color.RED)
layoutCompleted.set(false)
view.viewTreeObserver.addOnGlobalLayoutListener {
layoutCompleted.set(true)
}
if (slipperyWhenAdded) {
windowManager.addView(view, slipperyLayoutParams)
} else {
// Add the window with non-slippery params, and make it slippery via updateLayout
windowManager.addView(view, nonSlipperyLayoutParams)
}
}
waitForLayoutToComplete()
if (!slipperyWhenAdded) {
scenario.onActivity {
layoutCompleted.set(false)
windowManager.updateViewLayout(view, slipperyLayoutParams)
}
}
waitForLayoutToComplete()
PollingCheck.waitFor {
view.hasWindowFocus()
}
return view
}
private fun testWindowIsNotSlippery(slipperyWhenAdded: Boolean) {
// Start overlay (attacker) activity
// Attacker: create a window that is slippery and will capture the initial DOWN touch event,
// then will move itself out of the way, forcing the next MOVE event to go to the bottom
// window
val topView = addWindow(slipperyWhenAdded)
viewToRemove = topView
// Inject motion DOWN into the attacking activity. It will cause the activity to move to
// bottom right, which will make the next touch slip into the current window
assertBottomWindowDoesNotReceiveSlipperyTouch(topView)
}
/**
* Test a top window that tries to set FLAG_SLIPPERY when it is added to WindowManager
*/
@Test
@AsbSecurityTest(cveBugId = [157929241])
fun testWindowIsNotSlipperyWhenAdded() {
testWindowIsNotSlippery(true /* slipperyWhenAdded */)
}
/**
* Test a top window that tries to set FLAG_SLIPPERY during relayout
*/
@Test
@AsbSecurityTest(cveBugId = [157929241])
fun testWindowIsNotSlipperyAfterRelayout() {
testWindowIsNotSlippery(false /* slipperyWhenAdded */)
}
// ========================== Embedded window tests ============================================
private lateinit var mVr: SurfaceControlViewHost
private fun addEmbeddedHostWindow(): SurfaceView {
val surfaceView = SurfaceView(getInstrumentation().targetContext)
val surfaceCreated = CountDownLatch(1)
scenario.onActivity {
surfaceView.setZOrderOnTop(true)
// The color green should not be visible, but helps debug if there are any layout issues
// with the embedded view that will be positioned on top
surfaceView.setBackgroundColor(android.graphics.Color.GREEN)
surfaceView.viewTreeObserver.addOnGlobalLayoutListener {
layoutCompleted.set(true)
}
surfaceView.getHolder().addCallback(SurfaceCreatedCallback(surfaceCreated))
windowManager.addView(surfaceView, slipperyLayoutParams)
}
waitForLayoutToComplete()
surfaceCreated.await()
PollingCheck.waitFor {
surfaceView.hasWindowFocus()
}
return surfaceView
}
private fun addEmbeddedView(surfaceView: SurfaceView): View {
val embeddedViewDrawn = CountDownLatch(1)
val viewDrawnCallback = Runnable {
embeddedViewDrawn.countDown()
}
layoutCompleted.set(false)
val embeddedView = View(getInstrumentation().targetContext)
scenario.onActivity {
embeddedView.setOnTouchListener(OnTouchListener(surfaceView))
embeddedView.setBackgroundColor(android.graphics.Color.RED)
embeddedView.viewTreeObserver.addOnGlobalLayoutListener {
layoutCompleted.set(true)
}
embeddedView.viewTreeObserver.registerFrameCommitCallback(viewDrawnCallback)
mVr = SurfaceControlViewHost(it, it.getDisplay(), surfaceView.getHostToken())
mVr.setView(embeddedView, slipperyLayoutParams)
surfaceView.setChildSurfacePackage(mVr.getSurfacePackage())
embeddedView.invalidate()
}
embeddedViewDrawn.await()
embeddedView.viewTreeObserver.unregisterFrameCommitCallback(viewDrawnCallback)
waitForLayoutToComplete()
return embeddedView
}
/**
* Create an embedded slippery window and ensure it continues to receive touch after it moves
* away from the touched position.
*/
@Test
@AsbSecurityTest(cveBugId = [157929241])
fun testWindowlessWindowIsNotSlippery() {
val surfaceView = addEmbeddedHostWindow()
viewToRemove = surfaceView
// 'embeddedView' variable is used to retain the reference through the end of the test
@Suppress("UNUSED_VARIABLE") val embeddedView = addEmbeddedView(surfaceView)
assertBottomWindowDoesNotReceiveSlipperyTouch(surfaceView)
}
// ========================== Shared utility functions =========================================
private inner class OnTouchListener(relayoutView: View) : View.OnTouchListener {
val relayoutView = relayoutView
override fun onTouch(v: View, e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN) {
// Move the window out of the way by changing the gravity to bottom right
val wmlp = WindowManager.LayoutParams()
wmlp.copyFrom(slipperyLayoutParams)
wmlp.gravity = Gravity.BOTTOM or Gravity.RIGHT
layoutCompleted.set(false)
// Cannot always call 'updateViewLayout' for the incoming view, because in the
// embedded case, the provided embedded view is not attached to the window manager
// (and will therefore crash). Just use the view provided in the constructor.
windowManager.updateViewLayout(relayoutView, wmlp)
return true
}
eventsForTopWindow.add(MotionEvent.obtain(e))
return true
}
}
private fun assertBottomWindowDoesNotReceiveSlipperyTouch(topView: View) {
// Inject motion DOWN into the top view / window. It will cause the window to move to
// bottom right, which will make the next touch slip into the current window if the top
// window is actually slippery
val (x, y) = getViewCenterOnScreen(topView)
val downTime = SystemClock.uptimeMillis()
sendEvent(downTime, MotionEvent.ACTION_DOWN, x, y)
waitForLayoutToComplete()
sendEvent(downTime, MotionEvent.ACTION_MOVE, x + 1, y + 1)
scenario.onActivity {
// Bottom activity should not get any events
assertNull(it.getEvent())
// Top window should continue getting events.
assertAction(MotionEvent.ACTION_MOVE, eventsForTopWindow.poll())
assertNull(eventsForTopWindow.poll())
}
}
/**
* Wait until the bottom activity has window focus
*/
private fun waitForWindowFocusOnBottomActivity() {
PollingCheck.waitFor {
var activityHasWindowFocus = AtomicBoolean(false)
scenario.onActivity { activity -> run {
activityHasWindowFocus.set(activity.hasWindowFocus())
}
}
activityHasWindowFocus.get()
}
}
private fun waitForLayoutToComplete() {
PollingCheck.waitFor {
layoutCompleted.get()
}
getInstrumentation().uiAutomation.syncInputTransactions(true /*waitAnimations*/)
}
private fun setDimensionsToQuarterScreen() {
val bounds: Rect = windowManager.currentWindowMetrics.bounds
val width = (bounds.right - bounds.left) / 4
val height = (bounds.bottom - bounds.top) / 4
slipperyLayoutParams.width = width
slipperyLayoutParams.height = height
nonSlipperyLayoutParams.width = width
nonSlipperyLayoutParams.height = height
}
private fun sendEvent(downTime: Long, action: Int, x: Float, y: Float) {
val eventTime = when (action) {
MotionEvent.ACTION_DOWN -> downTime
else -> SystemClock.uptimeMillis()
}
val event = MotionEvent.obtain(downTime, eventTime, action, x, y, 0 /*metaState*/)
event.source = InputDevice.SOURCE_TOUCHSCREEN
getInstrumentation().uiAutomation.injectInputEvent(event, true /*sync*/)
}
companion object {
private val TAG = "FlagSlipperyTest"
}
}