blob: 7cb84b986fbe13e11aa63803c739962bf3b05835 [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.input.cts
import android.graphics.PointF
import android.os.SystemClock
import android.view.Gravity
import android.view.InputDevice
import android.view.InputEvent
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
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.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
private fun getViewCenterOnScreen(v: View): PointF {
val location = IntArray(2)
v.getLocationOnScreen(location)
val x = location[0].toFloat() + v.width / 2
val y = location[1].toFloat() + v.height / 2
return PointF(x, y)
}
@MediumTest
@RunWith(AndroidJUnit4::class)
class PointerCancelTest {
@get:Rule
val activityRule = ActivityScenarioRule<CaptureEventActivity>(CaptureEventActivity::class.java)
private lateinit var activity: CaptureEventActivity
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private lateinit var verifier: EventVerifier
@Before
fun setUp() {
activityRule.getScenario().onActivity {
activity = it
}
PollingCheck.waitFor { activity.hasWindowFocus() }
instrumentation.uiAutomation.syncInputTransactions()
verifier = EventVerifier(activity::getInputEvent)
}
/**
* Check that pointer cancel is received by the activity via injectInputEvent.
*/
@Test
fun testPointerCancelMotion() {
val downTime = SystemClock.uptimeMillis()
val pointerInDecorView = getViewCenterOnScreen(activity.window.decorView)
// Start a valid touch stream
sendEvent(downTime, MotionEvent.ACTION_DOWN, pointerInDecorView, true /*sync*/)
verifier.assertReceivedDown()
pointerInDecorView.offset(0f, 1f)
sendEvent(downTime, MotionEvent.ACTION_MOVE, pointerInDecorView, true /*sync*/)
verifier.assertReceivedMove()
val secondPointer = PointF(pointerInDecorView.x + 1, pointerInDecorView.y + 1)
sendPointersEvent(downTime, ACTION_POINTER_1_DOWN,
arrayOf(pointerInDecorView, secondPointer),
0 /*flags*/, true /*sync*/)
verifier.assertReceivedPointerDown(1)
sendPointersEvent(downTime, ACTION_POINTER_1_UP,
arrayOf(pointerInDecorView, secondPointer),
MotionEvent.FLAG_CANCELED, true /*sync*/)
verifier.assertReceivedPointerCancel(1)
sendEvent(downTime, MotionEvent.ACTION_UP, pointerInDecorView, true /*sync*/)
verifier.assertReceivedUp()
}
@Test
fun testPointerCancelForSplitTouch() {
val view = addFloatingWindow()
val pointerInFloating = getViewCenterOnScreen(view)
val downTime = SystemClock.uptimeMillis()
val pointerOutsideFloating = PointF(pointerInFloating.x + view.width / 2 + 1,
pointerInFloating.y + view.height / 2 + 1)
val eventsInFloating = LinkedBlockingQueue<InputEvent>()
view.setOnTouchListener { v, event ->
eventsInFloating.add(MotionEvent.obtain(event))
}
val verifierForFloating = EventVerifier { eventsInFloating.poll(5, TimeUnit.SECONDS) }
// First finger down (floating window)
sendEvent(downTime, MotionEvent.ACTION_DOWN, pointerInFloating, true /*sync*/)
verifierForFloating.assertReceivedDown()
// First finger move (floating window)
pointerInFloating.offset(0f, 1f)
sendEvent(downTime, MotionEvent.ACTION_MOVE, pointerInFloating, true /*sync*/)
verifierForFloating.assertReceivedMove()
// Second finger down (activity window)
sendPointersEvent(downTime, ACTION_POINTER_1_DOWN,
arrayOf(pointerInFloating, pointerOutsideFloating),
0 /*flags*/, true /*sync*/)
verifier.assertReceivedDown()
verifierForFloating.assertReceivedMove()
// ACTION_CANCEL with cancel flag (activity window)
sendPointersEvent(downTime, ACTION_POINTER_1_UP,
arrayOf(pointerInFloating, pointerOutsideFloating),
MotionEvent.FLAG_CANCELED, true /*sync*/)
verifier.assertReceivedCancel()
verifierForFloating.assertReceivedMove()
// First finger up (floating window)
sendEvent(downTime, MotionEvent.ACTION_UP, pointerInFloating, true /*sync*/)
verifierForFloating.assertReceivedUp()
}
private fun sendEvent(downTime: Long, action: Int, pt: PointF, sync: Boolean) {
val eventTime = when (action) {
MotionEvent.ACTION_DOWN -> downTime
else -> SystemClock.uptimeMillis()
}
val event = MotionEvent.obtain(downTime, eventTime, action, pt.x, pt.y, 0 /*metaState*/)
event.source = InputDevice.SOURCE_TOUCHSCREEN
instrumentation.uiAutomation.injectInputEvent(event, sync)
}
private fun sendPointersEvent(
downTime: Long,
action: Int,
pointers: Array<PointF>,
flags: Int,
sync: Boolean
) {
val eventTime = SystemClock.uptimeMillis()
val pointerCount = pointers.size
val properties = arrayOfNulls<MotionEvent.PointerProperties>(pointerCount)
val coords = arrayOfNulls<MotionEvent.PointerCoords>(pointerCount)
for (i in 0 until pointerCount) {
properties[i] = MotionEvent.PointerProperties()
properties[i]!!.id = i
properties[i]!!.toolType = MotionEvent.TOOL_TYPE_FINGER
coords[i] = MotionEvent.PointerCoords()
coords[i]!!.x = pointers[i].x
coords[i]!!.y = pointers[i].y
}
val event = MotionEvent.obtain(downTime, eventTime, action, pointerCount,
properties, coords, 0 /*metaState*/, 0 /*buttonState*/,
0f /*xPrecision*/, 0f /*yPrecision*/, 0 /*deviceId*/, 0 /*edgeFlags*/,
InputDevice.SOURCE_TOUCHSCREEN, flags)
instrumentation.uiAutomation.injectInputEvent(event, sync)
}
private fun addFloatingWindow(): View {
val view = View(instrumentation.targetContext)
val layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
layoutParams.x = 0
layoutParams.y = 0
layoutParams.width = 100
layoutParams.height = 100
layoutParams.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
activity.runOnUiThread {
view.setBackgroundColor(android.graphics.Color.RED)
activity.windowManager.addView(view, layoutParams)
}
PollingCheck.waitFor {
view.hasWindowFocus()
}
instrumentation.uiAutomation.syncInputTransactions()
return view
}
companion object {
const val ACTION_POINTER_1_DOWN = (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT) or
MotionEvent.ACTION_POINTER_DOWN
const val ACTION_POINTER_1_UP = (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT) or
MotionEvent.ACTION_POINTER_UP
}
}