1 /* <lambda>null2 * Copyright (C) 2024 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.car.carlauncher.datasources 18 19 import android.util.Log 20 import com.android.car.carlauncher.LauncherItemProto 21 import com.android.car.carlauncher.LauncherItemProto.LauncherItemMessage 22 import com.android.car.carlauncher.datasources.AppOrderDataSource.AppOrderInfo 23 import com.android.car.carlauncher.datastore.launcheritem.LauncherItemListSource 24 import kotlinx.coroutines.CoroutineDispatcher 25 import kotlinx.coroutines.Dispatchers 26 import kotlinx.coroutines.flow.Flow 27 import kotlinx.coroutines.flow.MutableStateFlow 28 import kotlinx.coroutines.flow.emitAll 29 import kotlinx.coroutines.flow.flow 30 import kotlinx.coroutines.flow.flowOn 31 import kotlinx.coroutines.flow.map 32 import kotlinx.coroutines.flow.onCompletion 33 import kotlinx.coroutines.flow.onStart 34 import kotlinx.coroutines.withContext 35 36 /** 37 * DataSource for managing the persisted order of apps. This class encapsulates all 38 * interactions with the persistent storage (e.g., Files, Proto, Database), acting as 39 * the single source of truth. 40 * 41 * Important: To ensure consistency, avoid modifying the persistent storage directly. 42 * Use the methods provided by this DataSource. 43 */ 44 interface AppOrderDataSource { 45 46 /** 47 * Saves the provided app order to persistent storage. 48 * 49 * @param appOrderInfoList The new order of apps to be saved, represented as a list of 50 * LauncherItemMessage objects. 51 */ 52 suspend fun saveAppOrder(appOrderInfoList: List<AppOrderInfo>) 53 54 /** 55 * Returns a Flow of the saved app order. The Flow will emit the latest saved order 56 * and any subsequent updates. 57 * 58 * @return A Flow of [AppOrderInfo] lists, representing the saved app order. 59 */ 60 fun getSavedAppOrder(): Flow<List<AppOrderInfo>> 61 62 /** 63 * Returns a Flow of comparators for sorting app lists. The comparators will prioritize the 64 * saved app order, and may fall back to other sorting logic if necessary. 65 * 66 * @return A Flow of Comparator objects, used to sort [AppOrderInfo] lists. 67 */ 68 fun getSavedAppOrderComparator(): Flow<Comparator<AppOrderInfo>> 69 70 /** 71 * Clears the saved app order from persistent storage. 72 * 73 * @return `true` if the operation was successful, `false` otherwise. 74 */ 75 suspend fun clearAppOrder(): Boolean 76 77 data class AppOrderInfo(val packageName: String, val className: String, val displayName: String) 78 } 79 80 /** 81 * Implementation of the [AppOrderDataSource] interface, responsible for managing app order 82 * persistence using a Proto file storage mechanism. 83 * 84 * @property launcherItemListSource The source for accessing and updating the raw Proto data. 85 * @property bgDispatcher (Optional) A CoroutineDispatcher specifying the thread pool for background 86 * operations (defaults to Dispatchers.IO for I/O-bound tasks). 87 */ 88 class AppOrderProtoDataSourceImpl( 89 private val launcherItemListSource: LauncherItemListSource, 90 private val bgDispatcher: CoroutineDispatcher = Dispatchers.IO, 91 ) : AppOrderDataSource { 92 93 private val appOrderFlow = MutableStateFlow(emptyList<AppOrderInfo>()) 94 95 /** 96 * Saves the current app order to a Proto file for persistent storage. 97 * * Performs the save operation on the background dispatcher ([bgDispatcher]). 98 * * Updates all collectors of [getSavedAppOrderComparator] and [getSavedAppOrder] immediately, 99 * even before the write operation has completed. 100 * * In case of a write failure, the operation fails silently. This might lead to a 101 * temporarily inconsistent app order for the current session (until the app restarts). 102 */ saveAppOrdernull103 override suspend fun saveAppOrder(appOrderInfoList: List<AppOrderInfo>) { 104 // Immediately update the cache. 105 appOrderFlow.value = appOrderInfoList 106 // Store the app order persistently. 107 withContext(bgDispatcher) { 108 // If it fails to write, it fails silently. 109 if (!launcherItemListSource.writeToFile( 110 LauncherItemProto.LauncherItemListMessage.newBuilder() 111 .addAllLauncherItemMessage(appOrderInfoList.mapIndexed { index, item -> 112 convertToMessage(item, index) 113 }).build() 114 ) 115 ) { 116 Log.i(TAG, "saveAppOrder failed to writeToFile") 117 } 118 } 119 } 120 121 /** 122 * Gets the latest know saved order to sort the apps. 123 * Also check [getSavedAppOrderComparator] if you need comparator to sort the list of apps. 124 * 125 * * Emits a new list to all collectors whenever the app order is updated using the 126 * [saveAppOrder] function or when [clearAppOrder] is called. 127 * 128 * __Handling Apps with Unknown Positions:__ 129 * The client should implement logic to handle apps whose positions are not 130 * specified in the saved order. A common strategy is to append them to the end of the list. 131 * 132 * __Handling Unavailable Apps:__ 133 * The client can choose to exclude apps that are unavailable (e.g., uninstalled or disabled) 134 * from the sorted list. 135 */ getSavedAppOrdernull136 override fun getSavedAppOrder(): Flow<List<AppOrderInfo>> = flow { 137 withContext(bgDispatcher) { 138 val appOrderFromFiles = launcherItemListSource.readFromFile()?.launcherItemMessageList 139 // Read from the persistent storage for pre-existing order. 140 // If no pre-existing order exists it initially returns an emptyList. 141 if (!appOrderFromFiles.isNullOrEmpty()) { 142 appOrderFlow.value = 143 appOrderFromFiles.sortedBy { it.relativePosition } 144 .map { AppOrderInfo(it.packageName, it.className, it.displayName) } 145 } 146 } 147 emitAll(appOrderFlow) 148 }.flowOn(bgDispatcher).onStart { 149 /** 150 * Ideally, the client of this flow should use [clearAppOrder] to 151 * delete/reset the app order. However, if the file gets deleted 152 * externally (e.g., by another API or process), we need to observe 153 * the deletion event and update the flow accordingly. 154 */ <lambda>null155 launcherItemListSource.attachFileDeletionObserver { 156 // When the file is deleted, reset the appOrderFlow to an empty list. 157 appOrderFlow.value = emptyList() 158 } <lambda>null159 }.onCompletion { 160 // Detach the observer to prevent leaks and unnecessary callbacks. 161 launcherItemListSource.detachFileDeletionObserver() 162 } 163 164 /** 165 * Provides a Flow of comparators to sort a list of apps. 166 * 167 * * Sorts apps based on a pre-defined order. If an app is not found in the pre-defined 168 * order, it falls back to alphabetical sorting with [AppOrderInfo.displayName]. 169 * * Emits a new comparator to all collectors whenever the app order is updated using the 170 * [saveAppOrder] function or when [clearAppOrder] is called. 171 * 172 * @see getSavedAppOrder 173 */ getSavedAppOrderComparatornull174 override fun getSavedAppOrderComparator(): Flow<Comparator<AppOrderInfo>> { 175 return getSavedAppOrder().map { appOrderInfoList -> 176 val appOrderMap = appOrderInfoList.withIndex().associateBy({ it.value }, { it.index }) 177 Comparator<AppOrderInfo> { app1, app2 -> 178 when { 179 // Both present in predefined list. 180 appOrderMap.contains(app1) && appOrderMap.contains(app2) -> { 181 // Kotlin compiler complains for nullability, although this should not be. 182 appOrderMap[app1]!! - appOrderMap[app2]!! 183 } 184 // Prioritize predefined names. 185 appOrderMap.contains(app1) -> -1 186 appOrderMap.contains(app2) -> 1 187 // Fallback to alphabetical. 188 else -> app1.displayName.compareTo(app2.displayName) 189 } 190 } 191 }.flowOn(bgDispatcher) 192 } 193 194 /** 195 * Deletes the persisted app order data. Performs the file deletion operation on the 196 * background dispatcher ([bgDispatcher]). 197 * 198 * * Successful deletion will report empty/default order [emptyList] to collectors of 199 * [getSavedAppOrder] amd [getSavedAppOrderComparator] 200 * 201 * @return `true` if the deletion was successful, `false` otherwise. 202 */ clearAppOrdernull203 override suspend fun clearAppOrder(): Boolean { 204 return withContext(bgDispatcher) { 205 launcherItemListSource.deleteFile() 206 }.also { 207 if (it) { 208 // If delete is successful report empty app order. 209 appOrderFlow.value = emptyList() 210 } 211 } 212 } 213 convertToMessagenull214 private fun convertToMessage( 215 appOrderInfo: AppOrderInfo, 216 relativePosition: Int 217 ): LauncherItemMessage? { 218 val builder = LauncherItemMessage.newBuilder().setPackageName(appOrderInfo.packageName) 219 .setClassName(appOrderInfo.className).setDisplayName(appOrderInfo.displayName) 220 .setRelativePosition(relativePosition).setContainerID(DOES_NOT_SUPPORT_CONTAINER) 221 return builder.build() 222 } 223 224 companion object { 225 val TAG: String = AppOrderDataSource::class.java.simpleName 226 private const val DOES_NOT_SUPPORT_CONTAINER = -1 227 } 228 } 229