[go: nahoru, domu]

blob: d2fed78291576db35850954eedd32f0afd5dea05 [file] [log] [blame]
/*
* Copyright 2020 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 androidx.compose.ui.input.pointer
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.ValueElement
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.withTimeout
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class SuspendingPointerInputFilterTest {
@After
fun after() {
// some tests may set this
isDebugInspectorInfoEnabled = false
}
@Test
fun testAwaitSingleEvent(): Unit = runBlockingTest {
val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
val result = CompletableDeferred<PointerEvent>()
launch {
with(filter) {
awaitPointerEventScope {
result.complete(awaitPointerEvent())
}
}
}
val emitter = PointerInputChangeEmitter()
val expectedChange = emitter.nextChange(Offset(5f, 5f))
filter.onPointerEvent(
expectedChange.toPointerEvent(),
PointerEventPass.Main,
IntSize(10, 10)
)
val receivedEvent = withTimeout(200) {
result.await()
}
assertEquals(expectedChange, receivedEvent.firstChange)
}
@Test
fun testAwaitSeveralEvents(): Unit = runBlockingTest {
val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
val results = Channel<PointerEvent>(Channel.UNLIMITED)
launch {
with(filter) {
awaitPointerEventScope {
repeat(3) {
results.offer(awaitPointerEvent())
}
results.close()
}
}
}
val emitter = PointerInputChangeEmitter()
val expected = listOf(
emitter.nextChange(Offset(5f, 5f)),
emitter.nextChange(Offset(10f, 5f)),
emitter.nextChange(Offset(10f, 10f))
)
val bounds = IntSize(20, 20)
expected.forEach {
filter.onPointerEvent(it.toPointerEvent(), PointerEventPass.Main, bounds)
}
val received = withTimeout(200) {
results.receiveAsFlow()
.map { it.firstChange }
.toList()
}
assertEquals(expected, received)
}
@Test
fun testSyntheticCancelEvent(): Unit = runBlockingTest {
val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
val results = Channel<PointerEvent>(Channel.UNLIMITED)
launch {
with(filter) {
awaitPointerEventScope {
repeat(3) {
results.offer(awaitPointerEvent())
}
results.close()
}
}
}
val bounds = IntSize(50, 50)
val emitter1 = PointerInputChangeEmitter(0)
val emitter2 = PointerInputChangeEmitter(1)
val expectedEvents = listOf(
PointerEvent(
listOf(
emitter1.nextChange(Offset(5f, 5f)),
emitter2.nextChange(Offset(10f, 10f))
)
),
PointerEvent(
listOf(
emitter1.nextChange(Offset(6f, 6f)),
emitter2.nextChange(Offset(10f, 10f), down = false)
)
),
// Synthetic cancel should look like this;
// only one pointer since the previous event's second pointer changed to up,
// the old position unchanged, 'down' changed from true to false, and the downChange
// marked as consumed.
PointerEvent(
listOf(
PointerInputChange(
PointerId(0),
0,
Offset(6f, 6f),
false,
0,
Offset(6f, 6f),
true,
consumed = ConsumedData(downChange = true)
)
)
)
)
expectedEvents.take(expectedEvents.size - 1).forEach {
filter.onPointerEvent(it, PointerEventPass.Main, bounds)
}
filter.onCancel()
val received = withTimeout(200) {
results.receiveAsFlow().toList()
}
assertThat(expectedEvents).hasSize(received.size)
expectedEvents.forEachIndexed { index, expectedEvent ->
val actualEvent = received[index]
PointerEventSubject.assertThat(actualEvent).isStructurallyEqualTo(expectedEvent)
}
}
@Test
fun testCancelledHandlerBlock() = runBlockingTest {
val filter = SuspendingPointerInputFilter(FakeViewConfiguration())
val counter = TestCounter()
val handler = launch {
with(filter) {
try {
awaitPointerEventScope {
try {
counter.expect(1, "about to call awaitPointerEvent")
awaitPointerEvent()
fail("awaitPointerEvent returned; should have thrown for cancel")
} finally {
counter.expect(3, "inner finally block running")
}
}
} finally {
counter.expect(4, "outer finally block running; inner finally should have run")
}
}
}
counter.expect(2, "before cancelling handler; awaitPointerEvent should be suspended")
handler.cancel()
counter.expect(5, "after cancelling; finally blocks should have run")
}
@Test
fun testInspectorValue() = runBlocking<Unit> {
isDebugInspectorInfoEnabled = true
val block: suspend PointerInputScope.() -> Unit = {}
val modifier = Modifier.pointerInput(block) as InspectableValue
assertThat(modifier.nameFallback).isEqualTo("pointerInput")
assertThat(modifier.valueOverride).isNull()
assertThat(modifier.inspectableElements.asIterable()).containsExactly(
ValueElement("block", block)
)
}
@OptIn(ExperimentalComposeApi::class)
@Test
@MediumTest
@Ignore // ignored due to a bug b/178013220
fun testRestartPointerInput() = runBlocking<Unit> {
var toAdd by mutableStateOf("initial")
val result = mutableListOf<String>()
val latch = CountDownLatch(2)
ActivityScenario.launch(TestActivity::class.java).use { scenario ->
scenario.moveToState(Lifecycle.State.CREATED)
scenario.onActivity {
it.setContent {
// Read the value in composition to change the lambda capture below
val toCapture = toAdd
Box(
Modifier.pointerInput {
result += toCapture
latch.countDown()
suspendCancellableCoroutine<Unit> {}
}
)
}
}
scenario.moveToState(Lifecycle.State.STARTED)
Snapshot.withMutableSnapshot {
toAdd = "secondary"
}
assertTrue("waiting for relaunch timed out", latch.await(1, TimeUnit.SECONDS))
assertEquals(
listOf("initial", "secondary"),
result
)
}
}
}
private fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
private val PointerEvent.firstChange get() = changes.first()
private class PointerInputChangeEmitter(id: Int = 0) {
val pointerId = PointerId(id.toLong())
var previousTime = 0L
var previousPosition = Offset.Zero
var previousPressed = false
fun nextChange(
position: Offset = Offset.Zero,
down: Boolean = true,
time: Long = 0
): PointerInputChange {
return PointerInputChange(
id = pointerId,
time,
position,
down,
previousTime,
previousPosition,
previousPressed,
consumed = ConsumedData()
).also {
previousTime = time
previousPosition = position
previousPressed = down
}
}
}
private class FakeViewConfiguration : ViewConfiguration {
override val longPressTimeoutMillis: Long
get() = 500
override val doubleTapTimeoutMillis: Long
get() = 300
override val doubleTapMinTimeMillis: Long
get() = 40
override val touchSlop: Float
get() = 18f
}
private class TestCounter {
private var count = 0
fun expect(checkpoint: Int, message: String = "(no message)") {
val expected = count + 1
if (checkpoint != expected) {
fail("out of order event $checkpoint, expected $expected, $message")
}
count = expected
}
}