• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.communal.data.db
18 
19 import android.content.ComponentName
20 import android.os.UserManager
21 import androidx.room.Dao
22 import androidx.room.Delete
23 import androidx.room.Query
24 import androidx.room.RoomDatabase
25 import androidx.room.Transaction
26 import androidx.room.Update
27 import androidx.sqlite.db.SupportSQLiteDatabase
28 import com.android.app.tracing.coroutines.launchTraced as launch
29 import com.android.systemui.communal.nano.CommunalHubState
30 import com.android.systemui.communal.shared.model.SpanValue
31 import com.android.systemui.communal.shared.model.toFixed
32 import com.android.systemui.communal.shared.model.toResponsive
33 import com.android.systemui.communal.widgets.CommunalWidgetHost
34 import com.android.systemui.communal.widgets.CommunalWidgetModule.Companion.DEFAULT_WIDGETS
35 import com.android.systemui.dagger.SysUISingleton
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.log.LogBuffer
38 import com.android.systemui.log.core.Logger
39 import com.android.systemui.log.dagger.CommunalLog
40 import com.android.systemui.user.domain.interactor.UserLockedInteractor
41 import javax.inject.Inject
42 import javax.inject.Named
43 import javax.inject.Provider
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.flow.Flow
46 import kotlinx.coroutines.flow.first
47 
48 /**
49  * Callback that will be invoked when the Room database is created. Then the database will be
50  * populated with pre-configured default widgets to be rendered in the glanceable hub.
51  */
52 @SysUISingleton
53 class DefaultWidgetPopulation
54 @Inject
55 constructor(
56     @Background private val bgScope: CoroutineScope,
57     private val communalWidgetHost: CommunalWidgetHost,
58     private val communalWidgetDaoProvider: Provider<CommunalWidgetDao>,
59     @Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>,
60     @CommunalLog logBuffer: LogBuffer,
61     private val userManager: UserManager,
62     private val userLockedInteractor: UserLockedInteractor,
63 ) : RoomDatabase.Callback() {
64     companion object {
65         private const val TAG = "DefaultWidgetPopulation"
66     }
67 
68     private val logger = Logger(logBuffer, TAG)
69 
70     /**
71      * Reason for skipping default widgets population. Do not skip if this value is
72      * [SkipReason.NONE].
73      */
74     private var skipReason = SkipReason.NONE
75 
76     override fun onCreate(db: SupportSQLiteDatabase) {
77         super.onCreate(db)
78 
79         if (skipReason != SkipReason.NONE) {
80             logger.i("Skipped populating default widgets. Reason: $skipReason")
81             return
82         }
83 
84         bgScope.launch {
85             userLockedInteractor.isUserUnlocked(userManager.mainUser).first { it }
86             populateDefaultWidgets()
87         }
88     }
89 
90     private fun populateDefaultWidgets() {
91         // Default widgets should be associated with the main user.
92         val user = userManager.mainUser ?: return
93 
94         val userSerialNumber = userManager.getUserSerialNumber(user.identifier)
95 
96         defaultWidgets.forEachIndexed { index, name ->
97             val provider = ComponentName.unflattenFromString(name)
98             provider?.let {
99                 val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
100                 id?.let {
101                     communalWidgetDaoProvider
102                         .get()
103                         .addWidget(
104                             widgetId = id,
105                             componentName = name,
106                             rank = index,
107                             userSerialNumber = userSerialNumber,
108                             spanY = SpanValue.Fixed(3),
109                         )
110                 }
111             }
112         }
113 
114         logger.i("Populated default widgets in the database.")
115     }
116 
117     /**
118      * Skip populating default widgets in the Glanceable Hub when the database is created. This has
119      * no effect if default widgets have been populated already.
120      *
121      * @param skipReason Reason for skipping the default widgets population.
122      */
123     fun skipDefaultWidgetsPopulation(skipReason: SkipReason) {
124         this.skipReason = skipReason
125     }
126 
127     /** Reason for skipping default widgets population. */
128     enum class SkipReason {
129         /** Do not skip. */
130         NONE,
131         /** Widgets are restored from a backup. */
132         RESTORED_FROM_BACKUP,
133     }
134 }
135 
136 @Dao
137 interface CommunalWidgetDao {
138     @Query(
139         "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
140             "ON communal_item_rank_table.uid = communal_widget_table.item_id " +
141             "ORDER BY communal_item_rank_table.rank ASC"
142     )
getWidgetsnull143     fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>
144 
145     @Query(
146         "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
147             "ON communal_item_rank_table.uid = communal_widget_table.item_id " +
148             "ORDER BY communal_item_rank_table.rank ASC"
149     )
150     fun getWidgetsNow(): Map<CommunalItemRank, CommunalWidgetItem>
151 
152     @Query("SELECT * FROM communal_widget_table WHERE widget_id = :id")
153     fun getWidgetByIdNow(id: Int): CommunalWidgetItem?
154 
155     @Delete fun deleteWidgets(vararg widgets: CommunalWidgetItem)
156 
157     @Query("DELETE FROM communal_item_rank_table WHERE uid = :itemId")
158     fun deleteItemRankById(itemId: Long)
159 
160     @Query(
161         "INSERT INTO communal_widget_table" +
162             "(widget_id, component_name, item_id, user_serial_number, span_y, span_y_new) " +
163             "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber, :spanY, :spanYNew)"
164     )
165     fun insertWidget(
166         widgetId: Int,
167         componentName: String,
168         itemId: Long,
169         userSerialNumber: Int,
170         spanY: Int,
171         spanYNew: Int,
172     ): Long
173 
174     @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
175     fun insertItemRank(rank: Int): Long
176 
177     @Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid")
178     fun updateItemRank(itemUid: Long, order: Int)
179 
180     @Update fun updateWidget(widget: CommunalWidgetItem)
181 
182     @Query("DELETE FROM communal_widget_table") fun clearCommunalWidgetsTable()
183 
184     @Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable()
185 
186     @Transaction
187     fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) {
188         widgetIdToRankMap.forEach { (id, rank) ->
189             val widget = getWidgetByIdNow(id)
190             if (widget != null) {
191                 updateItemRank(widget.itemId, rank)
192             }
193         }
194     }
195 
196     @Transaction
resizeWidgetnull197     fun resizeWidget(appWidgetId: Int, spanY: SpanValue, widgetIdToRankMap: Map<Int, Int>) {
198         val widget = getWidgetByIdNow(appWidgetId)
199         if (widget != null) {
200             updateWidget(
201                 widget.copy(spanY = spanY.toFixed().value, spanYNew = spanY.toResponsive().value)
202             )
203         }
204         updateWidgetOrder(widgetIdToRankMap)
205     }
206 
207     @Transaction
addWidgetnull208     fun addWidget(
209         widgetId: Int,
210         provider: ComponentName,
211         rank: Int? = null,
212         userSerialNumber: Int,
213         spanY: SpanValue,
214     ): Long {
215         return addWidget(
216             widgetId = widgetId,
217             componentName = provider.flattenToString(),
218             rank = rank,
219             userSerialNumber = userSerialNumber,
220             spanY = spanY,
221         )
222     }
223 
224     @Transaction
addWidgetnull225     fun addWidget(
226         widgetId: Int,
227         componentName: String,
228         rank: Int? = null,
229         userSerialNumber: Int,
230         spanY: SpanValue,
231     ): Long {
232         val widgets = getWidgetsNow()
233 
234         // If rank is not specified (null or less than 0), rank it last by finding the current
235         // maximum rank and increment by 1. If the new widget is the first widget, set rank to 0.
236         val newRank = rank?.takeIf { it >= 0 } ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0
237 
238         // Shift widgets after [rank], unless widget is added at the end.
239         if (rank != null) {
240             widgets.forEach { (rankEntry, widgetEntry) ->
241                 if (rankEntry.rank < newRank) return@forEach
242                 updateItemRank(widgetEntry.itemId, rankEntry.rank + 1)
243             }
244         }
245 
246         return insertWidget(
247             widgetId = widgetId,
248             componentName = componentName,
249             itemId = insertItemRank(newRank),
250             userSerialNumber = userSerialNumber,
251             spanY = spanY.toFixed().value,
252             spanYNew = spanY.toResponsive().value,
253         )
254     }
255 
256     @Transaction
deleteWidgetByIdnull257     fun deleteWidgetById(widgetId: Int): Boolean {
258         val widget =
259             getWidgetByIdNow(widgetId)
260                 ?: // no entry to delete from db
261                 return false
262 
263         deleteItemRankById(widget.itemId)
264         deleteWidgets(widget)
265         return true
266     }
267 
268     /** Wipes current database and restores the snapshot represented by [state]. */
269     @Transaction
restoreCommunalHubStatenull270     fun restoreCommunalHubState(state: CommunalHubState) {
271         clearCommunalWidgetsTable()
272         clearCommunalItemRankTable()
273 
274         state.widgets.forEach {
275             // Check if there is a new value to restore. If so, restore that new value.
276             val spanYResponsive = if (it.spanYNew != 0) SpanValue.Responsive(it.spanYNew) else null
277             // If no new value, restore any existing old values.
278             val spanY = spanYResponsive ?: SpanValue.Fixed(it.spanY.coerceIn(3, 6))
279 
280             addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber, spanY)
281         }
282     }
283 }
284