Merge "Fix AppFunctionRuntimeMedata.set/getEnabled" into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index bcad4633..b63f82b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -576,10 +576,6 @@
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
}
- private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
- return taskInfo.isFreeform() && taskInfo.isResizeable;
- }
-
private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
return;
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index f8d0588..8e6cb3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -22,6 +22,7 @@
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -34,6 +35,7 @@
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.battery.BatteryMeterViewController
@@ -41,6 +43,7 @@
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.qs.composefragment.ui.GridAnchor
import com.android.systemui.qs.panels.ui.compose.EditMode
import com.android.systemui.qs.panels.ui.compose.TileGrid
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
@@ -79,16 +82,11 @@
}
@Composable
- override fun ContentScope.Content(
- modifier: Modifier,
- ) {
+ override fun ContentScope.Content(modifier: Modifier) {
val viewModel =
rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() }
- OverlayShade(
- modifier = modifier,
- onScrimClicked = viewModel::onScrimClicked,
- ) {
+ OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) {
Column {
ExpandedShadeHeader(
viewModelFactory = viewModel.shadeHeaderViewModelFactory,
@@ -98,40 +96,36 @@
modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding),
)
- ShadeBody(
- viewModel = viewModel.quickSettingsContainerViewModel,
- )
+ ShadeBody(viewModel = viewModel.quickSettingsContainerViewModel)
}
}
}
}
@Composable
-fun ShadeBody(
- viewModel: QuickSettingsContainerViewModel,
-) {
+fun SceneScope.ShadeBody(viewModel: QuickSettingsContainerViewModel) {
val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle()
AnimatedContent(
targetState = isEditing,
- transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) }
+ transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) },
) { editing ->
if (editing) {
EditMode(
viewModel = viewModel.editModeViewModel,
- modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding)
+ modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding),
)
} else {
QuickSettingsLayout(
viewModel = viewModel,
- modifier = Modifier.sysuiResTag("quick_settings_panel")
+ modifier = Modifier.sysuiResTag("quick_settings_panel"),
)
}
}
}
@Composable
-private fun QuickSettingsLayout(
+private fun SceneScope.QuickSettingsLayout(
viewModel: QuickSettingsContainerViewModel,
modifier: Modifier = Modifier,
) {
@@ -143,15 +137,18 @@
BrightnessSliderContainer(
viewModel = viewModel.brightnessSliderViewModel,
modifier =
- Modifier.fillMaxWidth()
- .height(QuickSettingsShade.Dimensions.BrightnessSliderHeight),
+ Modifier.fillMaxWidth().height(QuickSettingsShade.Dimensions.BrightnessSliderHeight),
)
- TileGrid(
- viewModel = viewModel.tileGridViewModel,
- modifier =
- Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
- viewModel.editModeViewModel::startEditing,
- )
+ Box {
+ GridAnchor()
+ TileGrid(
+ viewModel = viewModel.tileGridViewModel,
+ modifier =
+ Modifier.fillMaxWidth()
+ .heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
+ viewModel.editModeViewModel::startEditing,
+ )
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 7203b61..6f20e70 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -78,8 +78,6 @@
Dispatchers.resetMain()
}
- // For now the state changes at 0.5f expansion. This will change once we implement animation
- // (and this test will fail)
@Test
fun qsExpansionValueChanges_correctExpansionState() =
with(kosmos) {
@@ -87,18 +85,27 @@
val expansionState by collectLastValue(underTest.expansionState)
underTest.qsExpansionValue = 0f
- assertThat(expansionState)
- .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS)
+ assertThat(expansionState!!.progress).isEqualTo(0f)
underTest.qsExpansionValue = 0.3f
- assertThat(expansionState)
- .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS)
-
- underTest.qsExpansionValue = 0.7f
- assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS)
+ assertThat(expansionState!!.progress).isEqualTo(0.3f)
underTest.qsExpansionValue = 1f
- assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS)
+ assertThat(expansionState!!.progress).isEqualTo(1f)
+ }
+ }
+
+ @Test
+ fun qsExpansionValueChanges_clamped() =
+ with(kosmos) {
+ testScope.testWithinLifecycle {
+ val expansionState by collectLastValue(underTest.expansionState)
+
+ underTest.qsExpansionValue = -1f
+ assertThat(expansionState!!.progress).isEqualTo(0f)
+
+ underTest.qsExpansionValue = 2f
+ assertThat(expansionState!!.progress).isEqualTo(1f)
}
}
@@ -110,7 +117,7 @@
testableContext.orCreateTestableResources.addOverride(
R.bool.config_use_large_screen_shade_header,
- true
+ true,
)
fakeConfigurationRepository.onConfigurationChange()
@@ -126,7 +133,7 @@
testableContext.orCreateTestableResources.addOverride(
R.bool.config_use_large_screen_shade_header,
- false
+ false,
)
fakeConfigurationRepository.onConfigurationChange()
diff --git a/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
new file mode 100644
index 0000000..62ab18b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
@@ -0,0 +1,321 @@
+/*
+ * 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.android.systemui.grid.ui.compose
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.CollectionItemInfo
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.collectionItemInfo
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.max
+
+/**
+ * Horizontal (non lazy) grid that supports [spans] for its elements.
+ *
+ * The elements will be laid down vertically first, and then by columns. So assuming LTR layout, it
+ * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 rows):
+ * ```
+ * 0 2 5
+ * 0 2 6
+ * 1 3 7
+ * 4
+ * ```
+ *
+ * where repeated numbers show larger span. If an element doesn't fit in a column due to its span,
+ * it will start a new column.
+ *
+ * Elements in [spans] must be in the interval `[1, rows]` ([rows] > 0), and the composables are
+ * associated with the corresponding span based on their index.
+ *
+ * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
+ * represent the collection as a list of elements.
+ */
+@Composable
+fun HorizontalSpannedGrid(
+ rows: Int,
+ columnSpacing: Dp,
+ rowSpacing: Dp,
+ spans: List<Int>,
+ modifier: Modifier = Modifier,
+ composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
+) {
+ SpannedGrid(
+ primarySpaces = rows,
+ crossAxisSpacing = rowSpacing,
+ mainAxisSpacing = columnSpacing,
+ spans = spans,
+ isVertical = false,
+ modifier = modifier,
+ composables = composables,
+ )
+}
+
+/**
+ * Horizontal (non lazy) grid that supports [spans] for its elements.
+ *
+ * The elements will be laid down horizontally first, and then by rows. So assuming LTR layout, it
+ * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 columns):
+ * ```
+ * 0 0 1
+ * 2 2 3 4
+ * 5 6 7
+ * ```
+ *
+ * where repeated numbers show larger span. If an element doesn't fit in a row due to its span, it
+ * will start a new row.
+ *
+ * Elements in [spans] must be in the interval `[1, columns]` ([columns] > 0), and the composables
+ * are associated with the corresponding span based on their index.
+ *
+ * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
+ * represent the collection as a list of elements.
+ */
+@Composable
+fun VerticalSpannedGrid(
+ columns: Int,
+ columnSpacing: Dp,
+ rowSpacing: Dp,
+ spans: List<Int>,
+ modifier: Modifier = Modifier,
+ composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
+) {
+ SpannedGrid(
+ primarySpaces = columns,
+ crossAxisSpacing = columnSpacing,
+ mainAxisSpacing = rowSpacing,
+ spans = spans,
+ isVertical = true,
+ modifier = modifier,
+ composables = composables,
+ )
+}
+
+@Composable
+private fun SpannedGrid(
+ primarySpaces: Int,
+ crossAxisSpacing: Dp,
+ mainAxisSpacing: Dp,
+ spans: List<Int>,
+ isVertical: Boolean,
+ modifier: Modifier = Modifier,
+ composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
+) {
+ val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing)
+ spans.forEachIndexed { index, span ->
+ check(span in 1..primarySpaces) {
+ "Span out of bounds. Span at index $index has value of $span which is outside of the " +
+ "expected rance of [1, $primarySpaces]"
+ }
+ }
+
+ if (isVertical) {
+ check(crossAxisSpacing >= 0.dp) { "Negative columnSpacing $crossAxisSpacing" }
+ check(mainAxisSpacing >= 0.dp) { "Negative rowSpacing $mainAxisSpacing" }
+ } else {
+ check(mainAxisSpacing >= 0.dp) { "Negative columnSpacing $mainAxisSpacing" }
+ check(crossAxisSpacing >= 0.dp) { "Negative rowSpacing $crossAxisSpacing" }
+ }
+
+ val totalMainAxisGroups: Int =
+ remember(primarySpaces, spans) {
+ var currentAccumulated = 0
+ var groups = 1
+ spans.forEach { span ->
+ if (currentAccumulated + span <= primarySpaces) {
+ currentAccumulated += span
+ } else {
+ groups += 1
+ currentAccumulated = span
+ }
+ }
+ groups
+ }
+
+ val slotPositionsAndSizesCache = remember {
+ object {
+ var sizes = IntArray(0)
+ var positions = IntArray(0)
+ }
+ }
+
+ Layout(
+ {
+ (0 until spans.size).map { spanIndex ->
+ Box(
+ Modifier.semantics {
+ collectionItemInfo =
+ if (isVertical) {
+ CollectionItemInfo(spanIndex, 1, 0, 1)
+ } else {
+ CollectionItemInfo(0, 1, spanIndex, 1)
+ }
+ }
+ ) {
+ composables(spanIndex)
+ }
+ }
+ },
+ modifier.semantics { collectionInfo = CollectionInfo(spans.size, 1) },
+ ) { measurables, constraints ->
+ check(measurables.size == spans.size)
+ val crossAxisSize = if (isVertical) constraints.maxWidth else constraints.maxHeight
+ check(crossAxisSize != Constraints.Infinity) { "Width must be constrained" }
+ if (slotPositionsAndSizesCache.sizes.size != primarySpaces) {
+ slotPositionsAndSizesCache.sizes = IntArray(primarySpaces)
+ slotPositionsAndSizesCache.positions = IntArray(primarySpaces)
+ }
+ calculateCellsCrossAxisSize(
+ crossAxisSize,
+ primarySpaces,
+ crossAxisSpacing.roundToPx(),
+ slotPositionsAndSizesCache.sizes,
+ )
+ val cellSizesInCrossAxis = slotPositionsAndSizesCache.sizes
+
+ // with is needed because of the double receiver (Density, Arrangement).
+ with(crossAxisArrangement) {
+ arrange(
+ crossAxisSize,
+ slotPositionsAndSizesCache.sizes,
+ LayoutDirection.Ltr,
+ slotPositionsAndSizesCache.positions,
+ )
+ }
+ val startPositions = slotPositionsAndSizesCache.positions
+
+ val mainAxisSpacingPx = mainAxisSpacing.roundToPx()
+ val mainAxisTotalGaps = (totalMainAxisGroups - 1) * mainAxisSpacingPx
+ val mainAxisSize = if (isVertical) constraints.maxHeight else constraints.maxWidth
+ val mainAxisElementConstraint =
+ if (mainAxisSize == Constraints.Infinity) {
+ Constraints.Infinity
+ } else {
+ max(0, (mainAxisSize - mainAxisTotalGaps) / totalMainAxisGroups)
+ }
+
+ val mainAxisSizes = IntArray(totalMainAxisGroups) { 0 }
+
+ var currentSlot = 0
+ var mainAxisGroup = 0
+ val placeables =
+ measurables.mapIndexed { index, measurable ->
+ val span = spans[index]
+ if (currentSlot + span > primarySpaces) {
+ currentSlot = 0
+ mainAxisGroup += 1
+ }
+ val crossAxisConstraint =
+ calculateWidth(cellSizesInCrossAxis, startPositions, currentSlot, span)
+ PlaceResult(
+ measurable.measure(
+ makeConstraint(
+ isVertical,
+ mainAxisElementConstraint,
+ crossAxisConstraint,
+ )
+ ),
+ currentSlot,
+ mainAxisGroup,
+ )
+ .also {
+ currentSlot += span
+ mainAxisSizes[mainAxisGroup] =
+ max(
+ mainAxisSizes[mainAxisGroup],
+ if (isVertical) it.placeable.height else it.placeable.width,
+ )
+ }
+ }
+
+ val mainAxisTotalSize = mainAxisTotalGaps + mainAxisSizes.sum()
+ val mainAxisStartingPoints =
+ mainAxisSizes.runningFold(0) { acc, value -> acc + value + mainAxisSpacingPx }
+ val height = if (isVertical) mainAxisTotalSize else crossAxisSize
+ val width = if (isVertical) crossAxisSize else mainAxisTotalSize
+
+ layout(width, height) {
+ placeables.forEach { (placeable, slot, mainAxisGroup) ->
+ val x =
+ if (isVertical) {
+ startPositions[slot]
+ } else {
+ mainAxisStartingPoints[mainAxisGroup]
+ }
+ val y =
+ if (isVertical) {
+ mainAxisStartingPoints[mainAxisGroup]
+ } else {
+ startPositions[slot]
+ }
+ placeable.placeRelative(x, y)
+ }
+ }
+ }
+}
+
+fun makeConstraint(isVertical: Boolean, mainAxisSize: Int, crossAxisSize: Int): Constraints {
+ return if (isVertical) {
+ Constraints(maxHeight = mainAxisSize, minWidth = crossAxisSize, maxWidth = crossAxisSize)
+ } else {
+ Constraints(maxWidth = mainAxisSize, minHeight = crossAxisSize, maxHeight = crossAxisSize)
+ }
+}
+
+private fun calculateWidth(sizes: IntArray, positions: IntArray, startSlot: Int, span: Int): Int {
+ val crossAxisSize =
+ if (span == 1) {
+ sizes[startSlot]
+ } else {
+ val endSlot = startSlot + span - 1
+ positions[endSlot] + sizes[endSlot] - positions[startSlot]
+ }
+ .coerceAtLeast(0)
+ return crossAxisSize
+}
+
+private fun calculateCellsCrossAxisSize(
+ gridSize: Int,
+ slotCount: Int,
+ spacingPx: Int,
+ outArray: IntArray,
+) {
+ check(outArray.size == slotCount)
+ val gridSizeWithoutSpacing = gridSize - spacingPx * (slotCount - 1)
+ val slotSize = gridSizeWithoutSpacing / slotCount
+ val remainingPixels = gridSizeWithoutSpacing % slotCount
+ outArray.indices.forEach { index ->
+ outArray[index] = slotSize + if (index < remainingPixels) 1 else 0
+ }
+}
+
+private data class PlaceResult(
+ val placeable: Placeable,
+ val slotIndex: Int,
+ val mainAxisGroup: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index af167d4..c174038 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -26,7 +26,6 @@
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
-import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -38,10 +37,14 @@
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
@@ -51,11 +54,18 @@
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transitions
import com.android.compose.modifiers.height
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
@@ -70,11 +80,17 @@
import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
+import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings
+import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
import com.android.systemui.qs.composefragment.ui.notificationScrimClip
+import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
import com.android.systemui.qs.flags.QSComposeFragment
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.panels.ui.compose.QuickQuickSettings
+import com.android.systemui.qs.shared.ui.ElementKeys
+import com.android.systemui.qs.ui.composable.QuickSettingsShade
import com.android.systemui.qs.ui.composable.QuickSettingsTheme
import com.android.systemui.qs.ui.composable.ShadeBody
import com.android.systemui.res.R
@@ -86,11 +102,13 @@
import java.util.function.Consumer
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@SuppressLint("ValidFragment")
@@ -166,33 +184,48 @@
setContent {
PlatformTheme {
val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
- val qsState by viewModel.expansionState.collectAsStateWithLifecycle()
AnimatedVisibility(
visible = visible,
modifier =
- Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
- notificationScrimClippingParams.isEnabled
- ) {
- Modifier.notificationScrimClip(
- notificationScrimClippingParams.leftInset,
- notificationScrimClippingParams.top,
- notificationScrimClippingParams.rightInset,
- notificationScrimClippingParams.bottom,
- notificationScrimClippingParams.radius,
- )
- },
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ .thenIf(notificationScrimClippingParams.isEnabled) {
+ Modifier.notificationScrimClip(
+ notificationScrimClippingParams.leftInset,
+ notificationScrimClippingParams.top,
+ notificationScrimClippingParams.rightInset,
+ notificationScrimClippingParams.bottom,
+ notificationScrimClippingParams.radius,
+ )
+ }
+ .graphicsLayer { elevation = 4.dp.toPx() },
) {
- AnimatedContent(targetState = qsState) {
- when (it) {
- QSFragmentComposeViewModel.QSExpansionState.QQS -> {
- QuickQuickSettingsElement()
- }
- QSFragmentComposeViewModel.QSExpansionState.QS -> {
- QuickSettingsElement()
- }
- else -> {}
- }
+ val sceneState = remember {
+ MutableSceneTransitionLayoutState(
+ viewModel.expansionState.value.toIdleSceneKey(),
+ transitions =
+ transitions {
+ from(QuickQuickSettings, QuickSettings) {
+ quickQuickSettingsToQuickSettings()
+ }
+ },
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ synchronizeQsState(
+ sceneState,
+ viewModel.expansionState.map { it.progress },
+ )
+ }
+
+ SceneTransitionLayout(
+ state = sceneState,
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ scene(QuickSettings) { QuickSettingsElement() }
+
+ scene(QuickQuickSettings) { QuickQuickSettingsElement() }
}
}
}
@@ -420,7 +453,7 @@
}
@Composable
- private fun QuickQuickSettingsElement() {
+ private fun SceneScope.QuickQuickSettingsElement() {
val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
val bottomPadding = dimensionResource(id = R.dimen.qqs_layout_padding_bottom)
DisposableEffect(Unit) {
@@ -450,8 +483,15 @@
viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel,
modifier =
Modifier.collapseExpandSemanticAction(
- stringResource(id = R.string.accessibility_quick_settings_expand)
- ),
+ stringResource(
+ id = R.string.accessibility_quick_settings_expand
+ )
+ )
+ .padding(
+ horizontal = {
+ QuickSettingsShade.Dimensions.Padding.roundToPx()
+ }
+ ),
)
}
}
@@ -460,7 +500,7 @@
}
@Composable
- private fun QuickSettingsElement() {
+ private fun SceneScope.QuickSettingsElement() {
val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top)
Column(
@@ -471,7 +511,10 @@
) {
val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle()
if (qsEnabled) {
- Box(modifier = Modifier.fillMaxSize().weight(1f)) {
+ Box(
+ modifier =
+ Modifier.element(ElementKeys.QuickSettingsContent).fillMaxSize().weight(1f)
+ ) {
Column {
Spacer(
modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }
@@ -483,7 +526,9 @@
FooterActions(
viewModel = viewModel.footerActionsViewModel,
qsVisibilityLifecycleOwner = this@QSFragmentCompose,
- modifier = Modifier.sysuiResTag("qs_footer_actions"),
+ modifier =
+ Modifier.sysuiResTag("qs_footer_actions")
+ .element(ElementKeys.FooterActions),
)
}
}
@@ -590,3 +635,85 @@
return currentId++
}
}
+
+object SceneKeys {
+ val QuickQuickSettings = SceneKey("QuickQuickSettingsScene")
+ val QuickSettings = SceneKey("QuickSettingsScene")
+
+ fun QSFragmentComposeViewModel.QSExpansionState.toIdleSceneKey(): SceneKey {
+ return when {
+ progress < 0.5f -> QuickQuickSettings
+ else -> QuickSettings
+ }
+ }
+}
+
+suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansion: Flow<Float>) {
+ coroutineScope {
+ val animationScope = this
+
+ var currentTransition: ExpansionTransition? = null
+
+ fun snapTo(scene: SceneKey) {
+ state.snapToScene(scene)
+ currentTransition = null
+ }
+
+ expansion.collectLatest { progress ->
+ when (progress) {
+ 0f -> snapTo(QuickQuickSettings)
+ 1f -> snapTo(QuickSettings)
+ else -> {
+ val transition = currentTransition
+ if (transition != null) {
+ transition.progress = progress
+ return@collectLatest
+ }
+
+ val newTransition =
+ ExpansionTransition(progress).also { currentTransition = it }
+ state.startTransitionImmediately(
+ animationScope = animationScope,
+ transition = newTransition,
+ )
+ }
+ }
+ }
+ }
+}
+
+private class ExpansionTransition(currentProgress: Float) :
+ TransitionState.Transition.ChangeScene(
+ fromScene = QuickQuickSettings,
+ toScene = QuickSettings,
+ ) {
+ override val currentScene: SceneKey
+ get() {
+ // This should return the logical scene. If the QS STLState is only driven by
+ // synchronizeQSState() then it probably does not matter which one we return, this is
+ // only used to compute the current user actions of a STL.
+ return QuickQuickSettings
+ }
+
+ override var progress: Float by mutableFloatStateOf(currentProgress)
+
+ override val progressVelocity: Float
+ get() = 0f
+
+ override val isInitiatedByUserInput: Boolean
+ get() = true
+
+ override val isUserInputOngoing: Boolean
+ get() = true
+
+ private val finishCompletable = CompletableDeferred<Unit>()
+
+ override suspend fun run() {
+ // This transition runs until it is interrupted by another one.
+ finishCompletable.await()
+ }
+
+ override fun freezeAndAnimateToCurrentState() {
+ finishCompletable.complete(Unit)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
new file mode 100644
index 0000000..1514986
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.android.systemui.qs.composefragment.ui
+
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.qs.shared.ui.ElementKeys
+
+fun TransitionBuilder.quickQuickSettingsToQuickSettings() {
+
+ fractionRange(start = 0.5f) { fade(ElementKeys.QuickSettingsContent) }
+
+ fractionRange(start = 0.9f) { fade(ElementKeys.FooterActions) }
+
+ anchoredTranslate(ElementKeys.QuickSettingsContent, ElementKeys.GridAnchor)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt
new file mode 100644
index 0000000..f0f46d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.android.systemui.qs.composefragment.ui
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.qs.shared.ui.ElementKeys
+
+/**
+ * This composable is used at the start of the tiles in QQS and QS to anchor the expansion and be
+ * able to have relative anchor translation of elements that appear in QS.
+ */
+@Composable
+fun SceneScope.GridAnchor(modifier: Modifier = Modifier) {
+ // The size of this anchor does not matter, as the tiles don't change size on expansion.
+ Spacer(modifier.element(ElementKeys.GridAnchor))
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 7ab11d2..7300ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -147,7 +147,7 @@
.stateIn(
lifecycleScope,
SharingStarted.WhileSubscribed(),
- disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled()
+ disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(),
)
private val _showCollapsedOnKeyguard = MutableStateFlow(false)
@@ -213,19 +213,11 @@
}
val expansionState: StateFlow<QSExpansionState> =
- combine(
- _stackScrollerOverscrolling,
- _qsExpanded,
- _qsExpansion,
- ) { args: Array<Any> ->
+ combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> ->
val expansion = args[2] as Float
- if (expansion > 0.5f) {
- QSExpansionState.QS
- } else {
- QSExpansionState.QQS
- }
+ QSExpansionState(expansion.coerceIn(0f, 1f))
}
- .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState.QQS)
+ .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f))
/**
* Accessibility action for collapsing/expanding QS. The provided runnable is responsible for
@@ -262,13 +254,6 @@
fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel
}
- sealed interface QSExpansionState {
- data object QQS : QSExpansionState
-
- data object QS : QSExpansionState
-
- @JvmInline value class Expanding(val progress: Float) : QSExpansionState
-
- @JvmInline value class Collapsing(val progress: Float) : QSExpansionState
- }
+ // In the future, this will have other relevant elements like squishiness.
+ data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index fd276c2..0c02b40 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
@@ -27,7 +28,7 @@
/** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */
interface GridLayout {
@Composable
- fun TileGrid(
+ fun SceneScope.TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
editModeStart: () -> Unit,
@@ -66,7 +67,7 @@
*/
fun splitInRows(
tiles: List<SizedTile<TileViewModel>>,
- columns: Int
+ columns: Int,
): List<List<SizedTile<TileViewModel>>> {
val row = TileRow<TileViewModel>(columns)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index 08a56bf..083f529 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -39,6 +39,7 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
@@ -55,7 +56,7 @@
@PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
) : GridLayout by delegateGridLayout {
@Composable
- override fun TileGrid(
+ override fun SceneScope.TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
editModeStart: () -> Unit,
@@ -85,16 +86,16 @@
) {
val page = pages[it]
- delegateGridLayout.TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+ with(delegateGridLayout) {
+ TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+ }
}
- Box(
- modifier = Modifier.height(FooterHeight).fillMaxWidth(),
- ) {
+ Box(modifier = Modifier.height(FooterHeight).fillMaxWidth()) {
PagerDots(
pagerState = pagerState,
activeColor = MaterialTheme.colorScheme.primary,
nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
- modifier = Modifier.align(Alignment.Center)
+ modifier = Modifier.align(Alignment.Center),
)
CompositionLocalProvider(value = LocalContentColor provides Color.White) {
IconButton(
@@ -103,7 +104,7 @@
) {
Icon(
imageVector = Icons.Default.Edit,
- contentDescription = stringResource(id = R.string.qs_edit)
+ contentDescription = stringResource(id = R.string.qs_edit),
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index f4acbec..8998a7f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -16,21 +16,28 @@
package com.android.systemui.qs.panels.ui.compose
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.util.fastMap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
+import com.android.systemui.qs.composefragment.ui.GridAnchor
import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile
-import com.android.systemui.qs.panels.ui.compose.infinitegrid.TileLazyGrid
import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
+import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
+import com.android.systemui.res.R
@Composable
-fun QuickQuickSettings(viewModel: QuickQuickSettingsViewModel, modifier: Modifier = Modifier) {
+fun SceneScope.QuickQuickSettings(
+ viewModel: QuickQuickSettingsViewModel,
+ modifier: Modifier = Modifier,
+) {
val sizedTiles by
viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList())
val tiles = sizedTiles.fastMap { it.tile }
@@ -41,20 +48,20 @@
onDispose { tiles.forEach { it.stopListening(token) } }
}
val columns by viewModel.columns.collectAsStateWithLifecycle()
-
- TileLazyGrid(
- modifier = modifier.sysuiResTag("qqs_tile_layout"),
- columns = GridCells.Fixed(columns),
- ) {
- items(
- sizedTiles.size,
- key = { index -> sizedTiles[index].tile.spec.spec },
- span = { index -> GridItemSpan(sizedTiles[index].width) },
- ) { index ->
+ Box(modifier = modifier) {
+ GridAnchor()
+ VerticalSpannedGrid(
+ columns = columns,
+ columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
+ rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
+ spans = sizedTiles.fastMap { it.width },
+ modifier = Modifier.sysuiResTag("qqs_tile_layout"),
+ ) { spanIndex ->
+ val it = sizedTiles[spanIndex]
Tile(
- tile = sizedTiles[index].tile,
- iconOnly = sizedTiles[index].isIcon,
- modifier = Modifier,
+ tile = it.tile,
+ iconOnly = it.isIcon,
+ modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
index 8c57d41..1a5297b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
@@ -20,16 +20,17 @@
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
@Composable
-fun TileGrid(
+fun SceneScope.TileGrid(
viewModel: TileGridViewModel,
modifier: Modifier = Modifier,
- editModeStart: () -> Unit
+ editModeStart: () -> Unit,
) {
val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle()
val tiles by viewModel.tileViewModels.collectAsStateWithLifecycle(emptyList())
- gridLayout.TileGrid(tiles, modifier, editModeStart)
+ with(gridLayout) { TileGrid(tiles, modifier, editModeStart) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 4946c01..8a96065 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -16,15 +16,17 @@
package com.android.systemui.qs.panels.ui.compose.infinitegrid
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.util.fastMap
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
import com.android.systemui.qs.panels.ui.compose.rememberEditListState
@@ -33,6 +35,8 @@
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
+import com.android.systemui.res.R
import javax.inject.Inject
@SysUISingleton
@@ -44,7 +48,7 @@
) : PaginatableGridLayout {
@Composable
- override fun TileGrid(
+ override fun SceneScope.TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
editModeStart: () -> Unit,
@@ -57,15 +61,18 @@
val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) }
- TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
- items(sizedTiles.size, span = { index -> GridItemSpan(sizedTiles[index].width) }) {
- index ->
- Tile(
- tile = sizedTiles[index].tile,
- iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec),
- modifier = Modifier,
- )
- }
+ VerticalSpannedGrid(
+ columns = columns,
+ columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
+ rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
+ spans = sizedTiles.fastMap { it.width },
+ ) { spanIndex ->
+ val it = sizedTiles[spanIndex]
+ Tile(
+ tile = it.tile,
+ iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
+ modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
new file mode 100644
index 0000000..625459d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.android.systemui.qs.shared.ui
+
+import com.android.compose.animation.scene.ElementKey
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Element keys to be used by the compose implementation of QS for animations. */
+object ElementKeys {
+ val QuickSettingsContent = ElementKey("QuickSettingsContent")
+ val GridAnchor = ElementKey("QuickSettingsGridAnchor")
+ val FooterActions = ElementKey("FooterActions")
+
+ class TileElementKey(spec: TileSpec, val position: Int) : ElementKey(spec.spec, spec.spec)
+
+ fun TileSpec.toElementKey(positionInGrid: Int) = TileElementKey(this, positionInGrid)
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
index 4104999..12b7445 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
@@ -16,6 +16,8 @@
package com.android.server.wm;
+import static android.tools.traces.Utils.busyWaitForDataSourceRegistration;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
@@ -28,7 +30,6 @@
import static java.io.File.createTempFile;
import static java.nio.file.Files.createTempDirectory;
-import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.Presubmit;
import android.tools.ScenarioBuilder;
import android.tools.traces.io.ResultWriter;
@@ -36,9 +37,6 @@
import android.view.Choreographer;
import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.After;
import org.junit.Before;
@@ -46,12 +44,9 @@
import org.junit.Test;
import org.mockito.Mockito;
-import perfetto.protos.PerfettoConfig.TracingServiceState;
import perfetto.protos.PerfettoConfig.WindowManagerConfig.LogFrequency;
-import java.io.FileInputStream;
import java.io.IOException;
-import java.util.Optional;
/**
* Test class for {@link WindowTracingPerfetto}.
@@ -74,7 +69,7 @@
sChoreographer = Mockito.mock(Choreographer.class);
sWindowTracing = new WindowTracingPerfetto(sWmMock, sChoreographer,
new WindowManagerGlobalLock(), TEST_DATA_SOURCE_NAME);
- waitDataSourceIsAvailable();
+ busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME);
}
@Before
@@ -156,67 +151,4 @@
mTraceMonitor.stop(writer);
}
-
- private static void waitDataSourceIsAvailable() {
- final int timeoutMs = 10000;
- final int busyWaitIntervalMs = 100;
-
- int elapsedMs = 0;
-
- while (!isDataSourceAvailable()) {
- try {
- Thread.sleep(busyWaitIntervalMs);
- elapsedMs += busyWaitIntervalMs;
- if (elapsedMs >= timeoutMs) {
- throw new RuntimeException("Data source didn't become available."
- + " Waited for: " + timeoutMs + " ms");
- }
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- }
-
- private static boolean isDataSourceAvailable() {
- byte[] proto = executeShellCommand("perfetto --query-raw");
-
- try {
- TracingServiceState state = TracingServiceState.parseFrom(proto);
-
- Optional<Integer> producerId = Optional.empty();
-
- for (TracingServiceState.Producer producer : state.getProducersList()) {
- if (producer.getPid() == android.os.Process.myPid()) {
- producerId = Optional.of(producer.getId());
- break;
- }
- }
-
- if (producerId.isEmpty()) {
- return false;
- }
-
- for (TracingServiceState.DataSource ds : state.getDataSourcesList()) {
- if (ds.getDsDescriptor().getName().equals(TEST_DATA_SOURCE_NAME)
- && ds.getProducerId() == producerId.get()) {
- return true;
- }
- }
- } catch (InvalidProtocolBufferException e) {
- throw new RuntimeException(e);
- }
-
- return false;
- }
-
- private static byte[] executeShellCommand(String command) {
- try {
- ParcelFileDescriptor fd = InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .executeShellCommand(command);
- FileInputStream is = new ParcelFileDescriptor.AutoCloseInputStream(fd);
- return is.readAllBytes();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
}
diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index cfb2645..6f3deab 100644
--- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -16,7 +16,7 @@
package com.android.internal.protolog;
-import static android.tools.traces.Utils.executeShellCommand;
+import static android.tools.traces.Utils.busyWaitForDataSourceRegistration;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -50,7 +50,6 @@
import com.android.internal.protolog.common.LogLevel;
import com.google.common.truth.Truth;
-import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.After;
import org.junit.Before;
@@ -60,14 +59,12 @@
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
-import perfetto.protos.PerfettoConfig.TracingServiceState;
import perfetto.protos.Protolog;
import perfetto.protos.ProtologCommon;
import java.io.File;
import java.io.IOException;
import java.util.List;
-import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@@ -184,7 +181,7 @@
TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
}
- waitDataSourceIsAvailable();
+ busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME);
}
@Before
@@ -870,54 +867,6 @@
.isEqualTo("This message should also be logged 567");
}
- private static void waitDataSourceIsAvailable() {
- final int timeoutMs = 10000;
- final int busyWaitIntervalMs = 100;
-
- int elapsedMs = 0;
-
- while (!isDataSourceAvailable()) {
- SystemClock.sleep(busyWaitIntervalMs);
- elapsedMs += busyWaitIntervalMs;
- if (elapsedMs >= timeoutMs) {
- throw new RuntimeException("Data source didn't become available."
- + " Waited for: " + timeoutMs + " ms");
- }
- }
- }
-
- private static boolean isDataSourceAvailable() {
- byte[] proto = executeShellCommand("perfetto --query-raw");
-
- try {
- TracingServiceState state = TracingServiceState.parseFrom(proto);
-
- Optional<Integer> producerId = Optional.empty();
-
- for (TracingServiceState.Producer producer : state.getProducersList()) {
- if (producer.getPid() == android.os.Process.myPid()) {
- producerId = Optional.of(producer.getId());
- break;
- }
- }
-
- if (producerId.isEmpty()) {
- return false;
- }
-
- for (TracingServiceState.DataSource ds : state.getDataSourcesList()) {
- if (ds.getDsDescriptor().getName().equals(TEST_PROTOLOG_DATASOURCE_NAME)
- && ds.getProducerId() == producerId.get()) {
- return true;
- }
- }
- } catch (InvalidProtocolBufferException e) {
- throw new RuntimeException(e);
- }
-
- return false;
- }
-
private enum TestProtoLogGroup implements IProtoLogGroup {
TEST_GROUP(true, true, false, "TEST_TAG");
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
index 675a59e..caa018d 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
@@ -256,7 +256,7 @@
"android.devicelock.IGetDeviceIdCallback",
"android.devicelock.IGetKioskAppsCallback",
"android.devicelock.IIsDeviceLockedCallback",
- "android.devicelock.IVoidResultCallback",
+ "android.devicelock.ILockUnlockDeviceCallback",
"android.federatedcompute.aidl.IExampleStoreCallback",
"android.federatedcompute.aidl.IExampleStoreIterator",
"android.federatedcompute.aidl.IExampleStoreIteratorCallback",
@@ -364,8 +364,6 @@
"android.health.connect.aidl.IGetPriorityResponseCallback",
"android.health.connect.aidl.IHealthConnectService",
"android.health.connect.aidl.IInsertRecordsResponseCallback",
- "android.health.connect.aidl.IMedicalDataSourceResponseCallback",
- "android.health.connect.aidl.IMedicalResourcesResponseCallback",
"android.health.connect.aidl.IMigrationCallback",
"android.health.connect.aidl.IReadMedicalResourcesResponseCallback",
"android.health.connect.aidl.IReadRecordsResponseCallback",
@@ -462,6 +460,7 @@
"android.net.ipmemorystore.IOnBlobRetrievedListener",
"android.net.ipmemorystore.IOnL2KeyResponseListener",
"android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener",
+ "android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener",
"android.net.ipmemorystore.IOnSameL3NetworkResponseListener",
"android.net.ipmemorystore.IOnStatusAndCountListener",
"android.net.ipmemorystore.IOnStatusListener",
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt
index d44c271..8d6e320 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt
@@ -57,7 +57,10 @@
ISSUE_MISSING_PERMISSION_ANNOTATION,
node,
context.getLocation(node),
- "The method ${node.name} is not permission-annotated."
+ """
+ ${node.name} should be annotated with either @EnforcePermission, \
+ @RequiresNoPermission or @PermissionManuallyEnforced.
+ """.trimMargin()
)
}
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
index 824be93..f985d02 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
@@ -73,7 +73,7 @@
.run()
.expect(
"""
- src/frameworks/base/services/java/com/android/server/Bar.java:3: Error: The method testMethod is not permission-annotated. [MissingPermissionAnnotation]
+ src/frameworks/base/services/java/com/android/server/Bar.java:3: Error: testMethod should be annotated with either @EnforcePermission, @RequiresNoPermission or @PermissionManuallyEnforced. [MissingPermissionAnnotation]
public void testMethod(int parameter1, int parameter2) { }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings