Merge changes Icc3721d9,I78357990 into main
* changes:
Move communal hub lazygrid logic to a separate function
Drag & drop to reorder and remove widget from glanceable hub
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 2ba1b77fb..e8ecd3a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -19,6 +19,8 @@
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -31,11 +33,13 @@
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@@ -45,7 +49,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -53,6 +56,7 @@
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.media.controls.ui.MediaHierarchyManager
import com.android.systemui.media.controls.ui.MediaHostState
import com.android.systemui.res.R
@@ -67,31 +71,12 @@
Box(
modifier = modifier.fillMaxSize().background(Color.White),
) {
- LazyHorizontalGrid(
- modifier = modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
- rows = GridCells.Fixed(CommunalContentSize.FULL.span),
- contentPadding = PaddingValues(horizontal = Dimensions.Spacing),
- horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
- verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
- ) {
- items(
- count = communalContent.size,
- key = { index -> communalContent[index].key },
- span = { index -> GridItemSpan(communalContent[index].size.span) },
- ) { index ->
- CommunalContent(
- modifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth),
- model = communalContent[index],
- viewModel = viewModel,
- deleteOnClick = if (viewModel.isEditMode) viewModel::onDeleteWidget else null,
- size =
- SizeF(
- Dimensions.CardWidth.value,
- communalContent[index].size.dp().value,
- ),
- )
- }
- }
+ CommunalHubLazyGrid(
+ modifier = Modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
+ communalContent = communalContent,
+ isEditMode = viewModel.isEditMode,
+ viewModel = viewModel,
+ )
if (viewModel.isEditMode && onOpenWidgetPicker != null) {
IconButton(onClick = onOpenWidgetPicker) {
Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text))
@@ -114,16 +99,80 @@
}
}
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun CommunalHubLazyGrid(
+ communalContent: List<CommunalContentModel>,
+ isEditMode: Boolean,
+ viewModel: BaseCommunalViewModel,
+ modifier: Modifier = Modifier,
+) {
+ var gridModifier = modifier
+ val gridState = rememberLazyGridState()
+ var list = communalContent
+ var dragDropState: GridDragDropState? = null
+ if (isEditMode && viewModel is CommunalEditModeViewModel) {
+ val contentListState = rememberContentListState(communalContent, viewModel)
+ list = contentListState.list
+ dragDropState = rememberGridDragDropState(gridState, contentListState)
+ gridModifier = gridModifier.dragContainer(dragDropState)
+ }
+ LazyHorizontalGrid(
+ modifier = gridModifier,
+ state = gridState,
+ rows = GridCells.Fixed(CommunalContentSize.FULL.span),
+ contentPadding = PaddingValues(horizontal = Dimensions.Spacing),
+ horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
+ verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
+ ) {
+ items(
+ count = list.size,
+ key = { index -> list[index].key },
+ span = { index -> GridItemSpan(list[index].size.span) },
+ ) { index ->
+ val cardModifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth)
+ val size =
+ SizeF(
+ Dimensions.CardWidth.value,
+ list[index].size.dp().value,
+ )
+ if (isEditMode && dragDropState != null) {
+ DraggableItem(dragDropState = dragDropState, enabled = true, index = index) {
+ isDragging ->
+ val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
+ CommunalContent(
+ modifier = cardModifier,
+ deleteOnClick = viewModel::onDeleteWidget,
+ elevation = elevation,
+ model = list[index],
+ viewModel = viewModel,
+ size = size,
+ )
+ }
+ } else {
+ CommunalContent(
+ modifier = cardModifier,
+ model = list[index],
+ viewModel = viewModel,
+ size = size,
+ )
+ }
+ }
+ }
+}
+
@Composable
private fun CommunalContent(
model: CommunalContentModel,
viewModel: BaseCommunalViewModel,
size: SizeF,
- deleteOnClick: ((id: Int) -> Unit)?,
modifier: Modifier = Modifier,
+ elevation: Dp = 0.dp,
+ deleteOnClick: ((id: Int) -> Unit)? = null,
) {
when (model) {
- is CommunalContentModel.Widget -> WidgetContent(model, size, deleteOnClick, modifier)
+ is CommunalContentModel.Widget ->
+ WidgetContent(model, size, elevation, deleteOnClick, modifier)
is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
is CommunalContentModel.Tutorial -> TutorialContent(modifier)
is CommunalContentModel.Umo -> Umo(viewModel, modifier)
@@ -134,22 +183,20 @@
private fun WidgetContent(
model: CommunalContentModel.Widget,
size: SizeF,
+ elevation: Dp,
deleteOnClick: ((id: Int) -> Unit)?,
modifier: Modifier = Modifier,
) {
// TODO(b/309009246): update background color
- Box(
+ Card(
modifier = modifier.fillMaxSize().background(Color.White),
+ elevation = CardDefaults.cardElevation(draggedElevation = elevation),
) {
if (deleteOnClick != null) {
IconButton(onClick = { deleteOnClick(model.appWidgetId) }) {
- Icon(
- Icons.Default.Close,
- LocalContext.current.getString(R.string.button_to_remove_widget)
- )
+ Icon(Icons.Default.Close, stringResource(R.string.button_to_remove_widget))
}
}
-
AndroidView(
modifier = modifier,
factory = { context ->
@@ -210,7 +257,7 @@
}
}
-private object Dimensions {
+object Dimensions {
val CardWidth = 464.dp
val CardHeightFull = 630.dp
val CardHeightHalf = 307.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
new file mode 100644
index 0000000..89c5765
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 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.communal.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+
+@Composable
+fun rememberContentListState(
+ communalContent: List<CommunalContentModel>,
+ viewModel: CommunalEditModeViewModel,
+): ContentListState {
+ return remember(communalContent) {
+ ContentListState(
+ communalContent,
+ viewModel::onDeleteWidget,
+ viewModel::onReorderWidgets,
+ )
+ }
+}
+
+/**
+ * Keeps the current state of the [CommunalContentModel] list being edited. [GridDragDropState]
+ * interacts with this class to update the order in the list. [onSaveList] should be called on
+ * dragging ends to persist the state in db for better performance.
+ */
+class ContentListState
+internal constructor(
+ communalContent: List<CommunalContentModel>,
+ private val onDeleteWidget: (id: Int) -> Unit,
+ private val onReorderWidgets: (ids: List<Int>) -> Unit,
+) {
+ var list by mutableStateOf(communalContent)
+ private set
+
+ /** Move item to a new position in the list. */
+ fun onMove(fromIndex: Int, toIndex: Int) {
+ list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
+ }
+
+ /** Remove widget from the list and the database. */
+ fun onRemove(indexToRemove: Int) {
+ if (list[indexToRemove] is CommunalContentModel.Widget) {
+ val widget = list[indexToRemove] as CommunalContentModel.Widget
+ list = list.toMutableList().apply { removeAt(indexToRemove) }
+ onDeleteWidget(widget.appWidgetId)
+ }
+ }
+
+ /** Persist the new order with all the movements happened during dragging. */
+ fun onSaveList() {
+ val widgetIds: List<Int> =
+ list.filterIsInstance<CommunalContentModel.Widget>().map { it.appWidgetId }
+ onReorderWidgets(widgetIds)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
new file mode 100644
index 0000000..6cfa2f2
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2023 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.communal.ui.compose
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
+import androidx.compose.foundation.lazy.grid.LazyGridItemScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+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.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+@Composable
+fun rememberGridDragDropState(
+ gridState: LazyGridState,
+ contentListState: ContentListState
+): GridDragDropState {
+ val scope = rememberCoroutineScope()
+ val state =
+ remember(gridState, contentListState) {
+ GridDragDropState(state = gridState, contentListState = contentListState, scope = scope)
+ }
+ LaunchedEffect(state) {
+ while (true) {
+ val diff = state.scrollChannel.receive()
+ gridState.scrollBy(diff)
+ }
+ }
+ return state
+}
+
+/**
+ * Handles drag and drop cards in the glanceable hub. While dragging to move, other items that are
+ * affected will dynamically get positioned and the state is tracked by [ContentListState]. When
+ * dragging to remove, affected cards will be moved and [ContentListState.onRemove] is called to
+ * remove the dragged item. On dragging ends, call [ContentListState.onSaveList] to persist the
+ * change.
+ */
+class GridDragDropState
+internal constructor(
+ private val state: LazyGridState,
+ private val contentListState: ContentListState,
+ private val scope: CoroutineScope,
+) {
+ var draggingItemIndex by mutableStateOf<Int?>(null)
+ private set
+
+ internal val scrollChannel = Channel<Float>()
+
+ private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
+ private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
+ internal val draggingItemOffset: Offset
+ get() =
+ draggingItemLayoutInfo?.let { item ->
+ draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
+ }
+ ?: Offset.Zero
+
+ private val draggingItemLayoutInfo: LazyGridItemInfo?
+ get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
+
+ internal fun onDragStart(offset: Offset) {
+ state.layoutInfo.visibleItemsInfo
+ .firstOrNull { item ->
+ item.isEditable &&
+ offset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+ offset.y.toInt() in item.offset.y..item.offsetEnd.y
+ }
+ ?.apply {
+ draggingItemIndex = index
+ draggingItemInitialOffset = this.offset.toOffset()
+ }
+ }
+
+ internal fun onDragInterrupted() {
+ if (draggingItemIndex != null) {
+ // persist list editing changes on dragging ends
+ contentListState.onSaveList()
+ draggingItemIndex = null
+ }
+ draggingItemDraggedDelta = Offset.Zero
+ draggingItemInitialOffset = Offset.Zero
+ }
+
+ internal fun onDrag(offset: Offset) {
+ draggingItemDraggedDelta += offset
+
+ val draggingItem = draggingItemLayoutInfo ?: return
+ val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
+ val endOffset = startOffset + draggingItem.size.toSize()
+ val middleOffset = startOffset + (endOffset - startOffset) / 2f
+
+ val targetItem =
+ state.layoutInfo.visibleItemsInfo.find { item ->
+ item.isEditable &&
+ middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+ middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
+ draggingItem.index != item.index
+ }
+
+ if (targetItem != null) {
+ val scrollToIndex =
+ if (targetItem.index == state.firstVisibleItemIndex) {
+ draggingItem.index
+ } else if (draggingItem.index == state.firstVisibleItemIndex) {
+ targetItem.index
+ } else {
+ null
+ }
+ if (scrollToIndex != null) {
+ scope.launch {
+ // this is needed to neutralize automatic keeping the first item first.
+ state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+ contentListState.onMove(draggingItem.index, targetItem.index)
+ }
+ } else {
+ contentListState.onMove(draggingItem.index, targetItem.index)
+ }
+ draggingItemIndex = targetItem.index
+ } else {
+ val overscroll = checkForOverscroll(startOffset, endOffset)
+ if (overscroll != 0f) {
+ scrollChannel.trySend(overscroll)
+ }
+ val removeOffset = checkForRemove(startOffset)
+ if (removeOffset != 0f) {
+ draggingItemIndex?.let {
+ contentListState.onRemove(it)
+ draggingItemIndex = null
+ }
+ }
+ }
+ }
+
+ private val LazyGridItemInfo.offsetEnd: IntOffset
+ get() = this.offset + this.size
+
+ /** Whether the grid item can be dragged or be a drop target. Only widget card is editable. */
+ private val LazyGridItemInfo.isEditable: Boolean
+ get() = contentListState.list[this.index] is CommunalContentModel.Widget
+
+ /** Calculate the amount dragged out of bound on both sides. Returns 0f if not overscrolled */
+ private fun checkForOverscroll(startOffset: Offset, endOffset: Offset): Float {
+ return when {
+ draggingItemDraggedDelta.x > 0 ->
+ (endOffset.x - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
+ draggingItemDraggedDelta.x < 0 ->
+ (startOffset.x - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
+ else -> 0f
+ }
+ }
+
+ // TODO(b/309968801): a temporary solution to decide whether to remove card when it's dragged up
+ // and out of grid. Once we have a taskbar, calculate the intersection of the dragged item with
+ // the Remove button.
+ private fun checkForRemove(startOffset: Offset): Float {
+ return if (draggingItemDraggedDelta.y < 0)
+ (startOffset.y + Dimensions.CardHeightHalf.value - state.layoutInfo.viewportStartOffset)
+ .coerceAtMost(0f)
+ else 0f
+ }
+}
+
+private operator fun IntOffset.plus(size: IntSize): IntOffset {
+ return IntOffset(x + size.width, y + size.height)
+}
+
+private operator fun Offset.plus(size: Size): Offset {
+ return Offset(x + size.width, y + size.height)
+}
+
+fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
+ return pointerInput(dragDropState) {
+ detectDragGesturesAfterLongPress(
+ onDrag = { change, offset ->
+ change.consume()
+ dragDropState.onDrag(offset = offset)
+ },
+ onDragStart = { offset -> dragDropState.onDragStart(offset) },
+ onDragEnd = { dragDropState.onDragInterrupted() },
+ onDragCancel = { dragDropState.onDragInterrupted() }
+ )
+ }
+}
+
+/** Wrap LazyGrid item with additional modifier needed for drag and drop. */
+@ExperimentalFoundationApi
+@Composable
+fun LazyGridItemScope.DraggableItem(
+ dragDropState: GridDragDropState,
+ index: Int,
+ enabled: Boolean,
+ modifier: Modifier = Modifier,
+ content: @Composable (isDragging: Boolean) -> Unit
+) {
+ if (!enabled) {
+ return Box(modifier = modifier) { content(false) }
+ }
+ val dragging = index == dragDropState.draggingItemIndex
+ val draggingModifier =
+ if (dragging) {
+ Modifier.zIndex(1f).graphicsLayer {
+ translationX = dragDropState.draggingItemOffset.x
+ translationY = dragDropState.draggingItemOffset.y
+ }
+ } else {
+ Modifier.animateItemPlacement()
+ }
+ Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
+ content(dragging)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index e50850d..a12db6f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -91,7 +91,8 @@
interface CommunalWidgetDao {
@Query(
"SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
- "ON communal_item_rank_table.uid = communal_widget_table.item_id"
+ "ON communal_item_rank_table.uid = communal_widget_table.item_id " +
+ "ORDER BY communal_item_rank_table.rank DESC"
)
fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>
@@ -112,6 +113,17 @@
@Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
fun insertItemRank(rank: Int): Long
+ @Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid")
+ fun updateItemRank(itemUid: Long, order: Int)
+
+ @Transaction
+ fun updateWidgetOrder(ids: List<Int>) {
+ ids.forEachIndexed { index, it ->
+ val widget = getWidgetByIdNow(it)
+ updateItemRank(widget.itemId, ids.size - index)
+ }
+ }
+
@Transaction
fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long {
return insertWidget(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index f7fee96..ded5581 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -61,6 +61,9 @@
/** Delete a widget by id from app widget service and the database. */
fun deleteWidget(widgetId: Int) {}
+
+ /** Update the order of widgets in the database. */
+ fun updateWidgetOrder(ids: List<Int>) {}
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -165,6 +168,15 @@
}
}
+ override fun updateWidgetOrder(ids: List<Int>) {
+ applicationScope.launch(bgDispatcher) {
+ communalWidgetDao.updateWidgetOrder(ids)
+ logger.i({ "Updated the order of widget list with ids: $str1." }) {
+ str1 = ids.toString()
+ }
+ }
+ }
+
private fun mapToContentModel(
entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
): CommunalWidgetContentModel {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 927bf02..fd7f641 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -84,6 +84,9 @@
/** Delete a widget by id. */
fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
+ /** Reorder widgets. The order in the list will be their display order in the hub. */
+ fun updateWidgetOrder(ids: List<Int>) = widgetRepository.updateWidgetOrder(ids)
+
/** A list of widget content to be displayed in the communal hub. */
val widgetContent: Flow<List<CommunalContentModel.Widget>> =
widgetRepository.communalWidgets.map { widgets ->
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 98f3594..b4ab5fb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -43,6 +43,9 @@
/** Called as the UI requests deleting a widget. */
open fun onDeleteWidget(id: Int) {}
+ /** Called as the UI requests reordering widgets. */
+ open fun onReorderWidgets(ids: List<Int>) {}
+
/** Called as the UI requests opening the widget editor. */
open fun onOpenWidgetEditor() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 14d9b2c..111f8b4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -41,4 +41,6 @@
communalInteractor.widgetContent
override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
+
+ override fun onReorderWidgets(ids: List<Int>) = communalInteractor.updateWidgetOrder(ids)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index 14ec4d4..16b2ed6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -124,6 +124,39 @@
assertThat(widgets()).containsExactly(communalItemRankEntry2, communalWidgetItemEntry2)
}
+ @Test
+ fun reorderWidget_emitsWidgetsInNewOrder() =
+ testScope.runTest {
+ val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
+ val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+ widgetsToAdd.forEach {
+ val (widgetId, provider, priority) = it
+ communalWidgetDao.addWidget(
+ widgetId = widgetId,
+ provider = provider,
+ priority = priority,
+ )
+ }
+ assertThat(widgets())
+ .containsExactly(
+ communalItemRankEntry1,
+ communalWidgetItemEntry1,
+ communalItemRankEntry2,
+ communalWidgetItemEntry2
+ )
+
+ val widgetIdsInNewOrder = listOf(widgetInfo2.widgetId, widgetInfo1.widgetId)
+ communalWidgetDao.updateWidgetOrder(widgetIdsInNewOrder)
+ assertThat(widgets())
+ .containsExactly(
+ communalItemRankEntry2,
+ communalWidgetItemEntry2,
+ communalItemRankEntry1,
+ communalWidgetItemEntry1
+ )
+ }
+
data class FakeWidgetMetadata(
val widgetId: Int,
val provider: ComponentName,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 28fae81..182712a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -202,6 +202,20 @@
}
@Test
+ fun reorderWidgets_queryDb() =
+ testScope.runTest {
+ userUnlocked(true)
+ val repository = initCommunalWidgetRepository()
+ runCurrent()
+
+ val ids = listOf(104, 103, 101)
+ repository.updateWidgetOrder(ids)
+ runCurrent()
+
+ verify(communalWidgetDao).updateWidgetOrder(ids)
+ }
+
+ @Test
fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() =
testScope.runTest {
communalEnabled(false)