1 /*
<lambda>null2 * Copyright (C) 2024 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 package com.example.tracing.demo
17
18 import android.os.Bundle
19 import android.os.Trace
20 import androidx.activity.ComponentActivity
21 import androidx.activity.compose.setContent
22 import androidx.activity.enableEdgeToEdge
23 import androidx.compose.animation.animateContentSize
24 import androidx.compose.animation.core.Spring
25 import androidx.compose.animation.core.spring
26 import androidx.compose.foundation.layout.Column
27 import androidx.compose.foundation.layout.Row
28 import androidx.compose.foundation.layout.RowScope
29 import androidx.compose.foundation.layout.padding
30 import androidx.compose.foundation.layout.safeDrawingPadding
31 import androidx.compose.foundation.lazy.LazyColumn
32 import androidx.compose.foundation.lazy.items
33 import androidx.compose.material.icons.Icons.Filled
34 import androidx.compose.material.icons.filled.ExpandLess
35 import androidx.compose.material.icons.filled.ExpandMore
36 import androidx.compose.material.icons.filled.PlayCircleOutline
37 import androidx.compose.material.icons.filled.StopCircle
38 import androidx.compose.material3.Card
39 import androidx.compose.material3.CardDefaults
40 import androidx.compose.material3.Icon
41 import androidx.compose.material3.IconButton
42 import androidx.compose.material3.MaterialTheme
43 import androidx.compose.material3.Surface
44 import androidx.compose.material3.Text
45 import androidx.compose.runtime.Composable
46 import androidx.compose.runtime.CompositionLocalProvider
47 import androidx.compose.runtime.compositionLocalOf
48 import androidx.compose.runtime.getValue
49 import androidx.compose.runtime.mutableStateListOf
50 import androidx.compose.runtime.mutableStateOf
51 import androidx.compose.runtime.remember
52 import androidx.compose.runtime.rememberCoroutineScope
53 import androidx.compose.runtime.setValue
54 import androidx.compose.ui.Modifier
55 import androidx.compose.ui.res.stringResource
56 import androidx.compose.ui.text.font.FontFamily
57 import androidx.compose.ui.text.font.FontStyle
58 import androidx.compose.ui.unit.dp
59 import com.android.app.tracing.coroutines.launchTraced as launch
60 import com.example.tracing.demo.experiments.Experiment
61 import com.example.tracing.demo.experiments.TRACK_NAME
62 import com.example.tracing.demo.ui.theme.BasicsCodelabTheme
63 import kotlin.random.Random
64 import kotlinx.coroutines.CancellationException
65 import kotlinx.coroutines.CoroutineDispatcher
66 import kotlinx.coroutines.cancelChildren
67 import kotlinx.coroutines.job
68
69 val AllExperiments = compositionLocalOf<List<Experiment>> { error("No Experiments found!") }
70
<lambda>null71 val Experiment = compositionLocalOf<Experiment> { error("No found!") }
72
73 val ExperimentLaunchDispatcher =
<lambda>null74 compositionLocalOf<CoroutineDispatcher> {
75 error("No @ExperimentLauncher CoroutineDispatcher found!")
76 }
77
78 class MainActivity(private val appComponent: ApplicationComponent) : ComponentActivity() {
onCreatenull79 override fun onCreate(savedInstanceState: Bundle?) {
80 super.onCreate(savedInstanceState)
81 enableEdgeToEdge()
82 setContent {
83 BasicsCodelabTheme {
84 CompositionLocalProvider(
85 AllExperiments provides appComponent.getExperimentList(),
86 ExperimentLaunchDispatcher provides
87 appComponent.getExperimentDefaultCoroutineDispatcher(),
88 ) {
89 DemoApp(modifier = Modifier.safeDrawingPadding())
90 }
91 }
92 }
93 }
94 }
95
96 @Composable
DemoAppnull97 fun DemoApp(modifier: Modifier = Modifier) {
98 Surface(modifier) { ExperimentList() }
99 }
100
101 @Composable
ExperimentListnull102 private fun ExperimentList(modifier: Modifier = Modifier) {
103 val allExperiments = AllExperiments.current
104
105 LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
106 items(items = allExperiments.stream().toList()) { experiment ->
107 CompositionLocalProvider(Experiment provides experiment) { ExperimentCard() }
108 }
109 }
110 }
111
112 @Composable
ExperimentCardnull113 private fun ExperimentCard(modifier: Modifier = Modifier) {
114 Card(
115 modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp),
116 colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
117 ) {
118 ExperimentContentRow()
119 }
120 }
121
122 @Composable
ExperimentContentRownull123 private fun ExperimentContentRow(modifier: Modifier = Modifier) {
124 Row(
125 modifier =
126 modifier
127 .padding(12.dp)
128 .animateContentSize(
129 animationSpec =
130 spring(
131 dampingRatio = Spring.DampingRatioNoBouncy,
132 stiffness = Spring.StiffnessMedium,
133 )
134 )
135 ) {
136 ExperimentContent()
137 }
138 }
139
140 @Composable
ExperimentContentnull141 private fun RowScope.ExperimentContent(modifier: Modifier = Modifier) {
142 val experiment = Experiment.current
143 val launcherDispatcher = ExperimentLaunchDispatcher.current
144 val scope = rememberCoroutineScope { launcherDispatcher + experiment.context }
145
146 var isRunning by remember { mutableStateOf(false) }
147 var expanded by remember { mutableStateOf(false) }
148 val statusMessages = remember { mutableStateListOf<String>() }
149 val className = experiment.javaClass.simpleName
150
151 Column(modifier = modifier.weight(1f).padding(12.dp)) {
152 Text(text = experiment.javaClass.simpleName, style = MaterialTheme.typography.headlineSmall)
153 Text(
154 text = experiment.description,
155 style = MaterialTheme.typography.bodyMedium.copy(fontStyle = FontStyle.Italic),
156 )
157 if (expanded) {
158 Text(
159 text = statusMessages.joinToString(separator = "\n") { it },
160 style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
161 )
162 }
163 }
164
165 IconButton(
166 onClick = {
167 if (isRunning) {
168 scope.coroutineContext.job.cancelChildren()
169 } else {
170 val cookie = Random.nextInt()
171 Trace.asyncTraceForTrackBegin(
172 Trace.TRACE_TAG_APP,
173 TRACK_NAME,
174 "Running: $className",
175 cookie,
176 )
177 statusMessages += "Started"
178 expanded = true
179 isRunning = true
180 scope
181 .launch("$className#runExperiment") { experiment.runExperiment() }
182 .invokeOnCompletion { cause ->
183 Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, cookie)
184 isRunning = false
185 statusMessages +=
186 when (cause) {
187 null -> "completed normally"
188 is CancellationException -> "cancelled normally: ${cause.message}"
189 else -> "failed"
190 }
191 }
192 }
193 }
194 ) {
195 Icon(
196 imageVector = if (isRunning) Filled.StopCircle else Filled.PlayCircleOutline,
197 contentDescription = stringResource(R.string.run_experiment),
198 )
199 }
200 IconButton(
201 onClick = {
202 expanded = !expanded
203 if (!expanded) statusMessages.clear()
204 },
205 enabled = !expanded || !isRunning,
206 ) {
207 Icon(
208 imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
209 contentDescription =
210 if (expanded) stringResource(R.string.show_less)
211 else stringResource(R.string.show_more),
212 )
213 }
214 }
215