1 /*
2 * Copyright 2021 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 androidx.compose.ui.inspection.rules
18
19 import android.view.View
20 import android.view.inspector.WindowInspector
21 import androidx.activity.ComponentActivity
22 import androidx.activity.compose.setContent
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.fillMaxSize
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.tooling.CompositionData
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.R
29 import androidx.compose.ui.layout.onGloballyPositioned
30 import androidx.compose.ui.platform.ViewRootForTest
31 import androidx.inspection.testing.InspectorTester
32 import androidx.test.core.app.ActivityScenario
33 import java.util.Collections
34 import java.util.WeakHashMap
35 import java.util.concurrent.CountDownLatch
36 import java.util.concurrent.TimeUnit
37 import kotlin.reflect.KClass
38 import kotlinx.coroutines.runBlocking
39 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Command
40 import layoutinspector.compose.inspection.LayoutInspectorComposeProtocol.Response
41 import org.junit.rules.ExternalResource
42
43 /**
44 * Test rule with common setup for compose inspector's tests:
45 * - it enables JVMTI;
46 * - starts a [clazz] activity;
47 * - enables inspection mode in compose in this activity;
48 * - starts a compose inspector itself if [useInspector] is `true`
49 *
50 * @param clazz an activity to start for a test
51 * @param useInspector parameter to enable / disable creation of inspector itself. By default, it is
52 * true. However a test may not need an inspector because it works with underlying infra, in such
53 * cases `false` can be passed to speed up test a bit.
54 */
55 class ComposeInspectionRule(
56 val clazz: KClass<out ComponentActivity>,
57 private val useInspector: Boolean = true
58 ) : ExternalResource() {
59 val rootsForTest = mutableListOf<ViewRootForTest>()
60 val roots = mutableListOf<View>()
61 val rootId: Long
62 get() = roots.single().uniqueDrawingId
63
64 lateinit var inspectorTester: InspectorTester
65 private set
66
67 @Suppress("UNCHECKED_CAST")
68 private val compositionDataSet: Collection<CompositionData>
69 get() =
70 rootsForTest.single().view.getTag(R.id.inspection_slot_table_set)
71 as Collection<CompositionData>
72
73 val compositionData: CompositionData
74 get() = compositionDataSet.first()
75
76 private lateinit var activityScenario: ActivityScenario<out ComponentActivity>
77
beforenull78 override fun before() {
79 JvmtiRule.ensureInitialised()
80 // need to set this special tag on the root view to enable inspection
81 ViewRootForTest.onViewCreatedCallback = {
82 rootsForTest.add(it)
83 it.view.setTag(
84 R.id.inspection_slot_table_set,
85 Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
86 )
87 }
88
89 activityScenario = ActivityScenario.launch(clazz.java)
90 activityScenario.onActivity { roots.addAll(WindowInspector.getGlobalWindowViews()) }
91 if (!useInspector) return
92 runBlocking { inspectorTester = InspectorTester("layoutinspector.compose.inspection") }
93 }
94
shownull95 fun show(composable: @Composable () -> Unit) = activityScenario.show(composable)
96
97 override fun after() {
98 if (useInspector) inspectorTester.dispose()
99 ViewRootForTest.onViewCreatedCallback = null
100 }
101 }
102
ActivityScenarionull103 fun ActivityScenario<out ComponentActivity>.show(composable: @Composable () -> Unit) {
104 val positionedLatch = CountDownLatch(1)
105 onActivity {
106 it.setContent {
107 Box(Modifier.fillMaxSize().onGloballyPositioned { positionedLatch.countDown() }) {
108 composable()
109 }
110 }
111 }
112 // Wait for the layout to be performed
113 positionedLatch.await(1, TimeUnit.SECONDS)
114
115 // Wait for the UI thread to complete its current work so we know that layout is done.
116 onActivity {}
117 }
118
sendCommandnull119 suspend fun InspectorTester.sendCommand(command: Command): Response {
120 return Response.parseFrom(sendCommand(command.toByteArray()))
121 }
122