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