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