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