blob: 65223790e72b5639321dec763c02be549eaa02a7 [file] [log] [blame]
/*
* Copyright (C) 2024 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 com.example.tracing.demo
import android.os.Bundle
import android.os.Trace
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.PlayCircleOutline
import androidx.compose.material.icons.filled.StopCircle
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import com.android.app.tracing.coroutines.launchTraced as launch
import com.example.tracing.demo.experiments.Experiment
import com.example.tracing.demo.experiments.TRACK_NAME
import com.example.tracing.demo.ui.theme.BasicsCodelabTheme
import kotlin.random.Random
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.job
val AllExperiments = compositionLocalOf<List<Experiment>> { error("No Experiments found!") }
val Experiment = compositionLocalOf<Experiment> { error("No found!") }
val ExperimentLaunchDispatcher =
compositionLocalOf<CoroutineDispatcher> {
error("No @ExperimentLauncher CoroutineDispatcher found!")
}
class MainActivity(private val appComponent: ApplicationComponent) : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BasicsCodelabTheme {
CompositionLocalProvider(
AllExperiments provides appComponent.getExperimentList(),
ExperimentLaunchDispatcher provides
appComponent.getExperimentDefaultCoroutineDispatcher(),
) {
DemoApp(modifier = Modifier.safeDrawingPadding())
}
}
}
}
}
@Composable
fun DemoApp(modifier: Modifier = Modifier) {
Surface(modifier) { ExperimentList() }
}
@Composable
private fun ExperimentList(modifier: Modifier = Modifier) {
val allExperiments = AllExperiments.current
LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
items(items = allExperiments.stream().toList()) { experiment ->
CompositionLocalProvider(Experiment provides experiment) { ExperimentCard() }
}
}
}
@Composable
private fun ExperimentCard(modifier: Modifier = Modifier) {
Card(
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
) {
ExperimentContentRow()
}
}
@Composable
private fun ExperimentContentRow(modifier: Modifier = Modifier) {
Row(
modifier =
modifier
.padding(12.dp)
.animateContentSize(
animationSpec =
spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium,
)
)
) {
ExperimentContent()
}
}
@Composable
private fun RowScope.ExperimentContent(modifier: Modifier = Modifier) {
val experiment = Experiment.current
val launcherDispatcher = ExperimentLaunchDispatcher.current
val scope = rememberCoroutineScope { launcherDispatcher + experiment.context }
var isRunning by remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
val statusMessages = remember { mutableStateListOf<String>() }
val className = experiment.javaClass.simpleName
Column(modifier = modifier.weight(1f).padding(12.dp)) {
Text(text = experiment.javaClass.simpleName, style = MaterialTheme.typography.headlineSmall)
Text(
text = experiment.description,
style = MaterialTheme.typography.bodyMedium.copy(fontStyle = FontStyle.Italic),
)
if (expanded) {
Text(
text = statusMessages.joinToString(separator = "\n") { it },
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
)
}
}
IconButton(
onClick = {
if (isRunning) {
scope.coroutineContext.job.cancelChildren()
} else {
val cookie = Random.nextInt()
Trace.asyncTraceForTrackBegin(
Trace.TRACE_TAG_APP,
TRACK_NAME,
"Running: $className",
cookie,
)
statusMessages += "Started"
expanded = true
isRunning = true
scope
.launch("$className#runExperiment") { experiment.runExperiment() }
.invokeOnCompletion { cause ->
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, cookie)
isRunning = false
statusMessages +=
when (cause) {
null -> "completed normally"
is CancellationException -> "cancelled normally: ${cause.message}"
else -> "failed"
}
}
}
}
) {
Icon(
imageVector = if (isRunning) Filled.StopCircle else Filled.PlayCircleOutline,
contentDescription = stringResource(R.string.run_experiment),
)
}
IconButton(
onClick = {
expanded = !expanded
if (!expanded) statusMessages.clear()
},
enabled = !expanded || !isRunning,
) {
Icon(
imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
contentDescription =
if (expanded) stringResource(R.string.show_less)
else stringResource(R.string.show_more),
)
}
}