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