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