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.repositories
18 
19 import android.content.ComponentName
20 import android.content.Intent
21 import android.content.pm.PackageManager
22 import android.content.pm.ResolveInfo
23 import android.graphics.drawable.Drawable
24 import com.android.car.carlauncher.AppItem
25 import com.android.car.carlauncher.AppMetaData
26 import com.android.car.carlauncher.datasources.AppOrderDataSource
27 import com.android.car.carlauncher.datasources.AppOrderDataSource.AppOrderInfo
28 import com.android.car.carlauncher.datasources.ControlCenterMirroringDataSource
29 import com.android.car.carlauncher.datasources.LauncherActivitiesDataSource
30 import com.android.car.carlauncher.datasources.MediaTemplateAppsDataSource
31 import com.android.car.carlauncher.datasources.UXRestrictionDataSource
32 import com.android.car.carlauncher.datasources.restricted.DisabledAppsDataSource
33 import com.android.car.carlauncher.datasources.restricted.TosDataSource
34 import com.android.car.carlauncher.datasources.restricted.TosState
35 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory
36 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType
37 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.DISABLED
38 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.LAUNCHER
39 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.MEDIA
40 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.MIRRORING
41 import com.android.car.carlauncher.repositories.appactions.AppLaunchProviderFactory.AppLauncherProviderType.TOS_DISABLED
42 import com.android.car.carlauncher.repositories.appactions.AppShortcutsFactory
43 import kotlinx.coroutines.CoroutineDispatcher
44 import kotlinx.coroutines.flow.Flow
45 import kotlinx.coroutines.flow.combine
46 import kotlinx.coroutines.flow.distinctUntilChanged
47 import kotlinx.coroutines.flow.flowOn
48 import kotlinx.coroutines.flow.map
49 
50 interface AppGridRepository {
51 
52     /**
53      * Returns a flow of all applications available in the app grid, including
54      * system apps, media apps, and potentially restricted apps.
55      *
56      * @return A Flow emitting a list of AppItem objects.
57      */
58     fun getAllAppsList(): Flow<List<AppItem>>
59 
60     /**
61      * Provides a flow indicating whether distraction optimization is required for the device.
62      * Distraction optimization might limit the features or visibility of apps.
63      *
64      * @return A Flow emitting a Boolean value, where `true` indicates distraction optimization is
65      * needed.
66      */
67     fun requiresDistractionOptimization(): Flow<Boolean>
68 
69     /**
70      * Returns a continuous flow representing the Terms of Service (ToS) state for apps.
71      * This state may determine the availability or restrictions of certain apps.
72      *
73      * @return A Flow emitting the current TosState.
74      */
75     fun getTosState(): Flow<TosState>
76 
77     /**
78      * Suspends execution to save the provided app order to persistent storage.
79      * Used to maintain the arrangement of apps within the app grid.
80      *
81      * @param currentAppOrder A list of AppItem representing the desired app order.
82      */
83     suspend fun saveAppOrder(currentAppOrder: List<AppItem>)
84 
85     /**
86      * Returns a flow of media-related apps installed on the device.
87      *
88      * @return A Flow emitting a list of AppItem representing media applications.
89      */
90     fun getMediaAppsList(): Flow<List<AppItem>>
91 }
92 
93 /**
94  * The core implementation of the AppGridRepository interface.  This class is responsible for:
95  *
96  * *  Fetching and combining app information from various sources (launcher activities,
97  *    media services, disabled apps, etc.)
98  * *  Applying restrictions and filtering app lists based on distraction optimization, ToS status,
99  *    and mirroring state.
100  * *  Managing app order and saving it to persistent storage.
101  * *  Providing real-time updates of the app grid as changes occur.
102  */
103 class AppGridRepositoryImpl(
104     private val launcherActivities: LauncherActivitiesDataSource,
105     private val mediaTemplateApps: MediaTemplateAppsDataSource,
106     private val disabledApps: DisabledAppsDataSource,
107     private val tosApps: TosDataSource,
108     private val controlCenterMirroring: ControlCenterMirroringDataSource,
109     private val uxRestriction: UXRestrictionDataSource,
110     private val appOrder: AppOrderDataSource,
111     private val packageManager: PackageManager,
112     private val appLaunchFactory: AppLaunchProviderFactory,
113     private val appShortcutsFactory: AppShortcutsFactory,
114     private val bgDispatcher: CoroutineDispatcher
115 ) : AppGridRepository {
116 
117     /**
118      * Provides a flow of all apps in the app grid.
119      * It combines data from multiple sources, filters apps based on restrictions, handles dynamic
120      * updates and returns the list in the last known savedOrder.
121      *
122      * @return A Flow emitting lists of AppItem objects.
123      */
getAllAppsListnull124     override fun getAllAppsList(): Flow<List<AppItem>> {
125         return combine(
126             getAllLauncherAndMediaApps(),
127             getRestrictedApps(),
128             controlCenterMirroring.getAppMirroringSession(),
129             appOrder.getSavedAppOrderComparator(),
130             uxRestriction.isDistractionOptimized()
131         ) { apps, restrictedApps, mirroringSession, order, isDistractionOptimized ->
132             val alreadyAddedComponents = apps.map { it.componentName.packageName }.toSet()
133             return@combine (apps + restrictedApps.filterNot {
134                 it.componentName.packageName in alreadyAddedComponents
135             }).sortedWith { a1, a2 ->
136                 order.compare(a1.appOrderInfo, a2.appOrderInfo)
137             }.map {
138                 if (mirroringSession.packageName == it.componentName.packageName) {
139                     it.redirectIntent = mirroringSession.launchIntent
140                 } else if (it.launchActionType == MIRRORING) {
141                     it.redirectIntent = null
142                 }
143                 it.toAppItem(isDistractionOptimized(it.componentName, it.launchActionType == MEDIA))
144             }
145         }.flowOn(bgDispatcher).distinctUntilChanged()
146     }
147 
148     /**
149      * Emitting distraction optimization status changes.
150      *
151      * @return A Flow of Boolean values, where `true` indicates distraction optimization is
152      * required.
153      */
requiresDistractionOptimizationnull154     override fun requiresDistractionOptimization(): Flow<Boolean> {
155         return uxRestriction.requiresDistractionOptimization()
156     }
157 
158     /**
159      * Provides the Terms of Service state for apps.
160      *
161      * @return A Flow emitting the current TosState.
162      */
getTosStatenull163     override fun getTosState(): Flow<TosState> {
164         return tosApps.getTosState()
165     }
166 
167     /**
168      *  Suspends saving the given app order to persistent storage.
169      *  Updates to the app order are posted to the subscribers of
170      *  [AppGridRepositoryImpl.getAllAppsList]
171      *
172      * @param currentAppOrder A list of AppItem representing the desired app order.
173      */
saveAppOrdernull174     override suspend fun saveAppOrder(currentAppOrder: List<AppItem>) {
175         appOrder.saveAppOrder(currentAppOrder.toAppOrderInfoList())
176     }
177 
178     /**
179      * Providing a flow of media-related apps.
180      * Handles dynamic updates to the list of media apps.
181      *
182      * @return A Flow emitting lists of AppItem objects representing media apps.
183      */
getMediaAppsListnull184     override fun getMediaAppsList(): Flow<List<AppItem>> {
185         return launcherActivities.getOnPackagesChanged().map {
186             mediaTemplateApps.getAllMediaServices(true).map {
187                 it.toAppInfo(MEDIA).toAppItem(true)
188             }
189         }.flowOn(bgDispatcher).distinctUntilChanged()
190     }
191 
getAllLauncherAndMediaAppsnull192     private fun getAllLauncherAndMediaApps(): Flow<List<AppInfo>> {
193         return launcherActivities.getOnPackagesChanged().map {
194             val launcherApps = launcherActivities.getAllLauncherActivities().map {
195                 AppInfo(it.label, it.componentName, it.getBadgedIcon(0), LAUNCHER)
196             }
197             val mediaTemplateApps = mediaTemplateApps.getAllMediaServices(false).map {
198                 it.toAppInfo(MEDIA)
199             }
200             launcherApps + mediaTemplateApps
201         }.flowOn(bgDispatcher).distinctUntilChanged()
202     }
203 
getRestrictedAppsnull204     private fun getRestrictedApps(): Flow<List<AppInfo>> {
205         return disabledApps.getDisabledApps()
206             .combine(tosApps.getTosState()) { disabledApps, tosApps ->
207                 return@combine disabledApps.map {
208                     it.toAppInfo(DISABLED)
209                 } + tosApps.restrictedApps.map {
210                     it.toAppInfo(TOS_DISABLED)
211                 }
212             }.flowOn(bgDispatcher).distinctUntilChanged()
213     }
214 
215     private data class AppInfo(
216         val displayName: CharSequence,
217         val componentName: ComponentName,
218         val icon: Drawable,
219         private val _launchActionType: AppLauncherProviderType,
220         var redirectIntent: Intent? = null
221     ) {
222         val launchActionType get() = if (redirectIntent == null) {
223             _launchActionType
224         } else {
225             MIRRORING
226         }
227 
228         val appOrderInfo =
229             AppOrderInfo(componentName.packageName, componentName.className, displayName.toString())
230     }
231 
toAppItemnull232     private fun AppInfo.toAppItem(isDistractionOptimized: Boolean): AppItem {
233         val metaData = AppMetaData(
234             displayName,
235             componentName,
236             icon,
237             isDistractionOptimized,
238             launchActionType == MIRRORING,
239             launchActionType == TOS_DISABLED,
240             { context ->
241                 appLaunchFactory
242                     .get(launchActionType)
243                     ?.launch(context, componentName, redirectIntent)
244             },
245             { contextViewPair ->
246                 appShortcutsFactory.showShortcuts(
247                     componentName,
248                     displayName,
249                     contextViewPair.first,
250                     contextViewPair.second
251                 )
252             }
253         )
254         return AppItem(metaData)
255     }
256 
ResolveInfonull257     private fun ResolveInfo.toAppInfo(launchActionType: AppLauncherProviderType): AppInfo {
258         val componentName: ComponentName
259         val icon: Drawable
260         if (launchActionType == MEDIA) {
261             componentName = ComponentName(serviceInfo.packageName, serviceInfo.name)
262             icon = serviceInfo.loadIcon(packageManager)
263         } else {
264             componentName = ComponentName(activityInfo.packageName, activityInfo.name)
265             icon = activityInfo.loadIcon(packageManager)
266         }
267         return AppInfo(
268             loadLabel(packageManager),
269             componentName,
270             icon,
271             launchActionType
272         )
273     }
274 
toAppOrderInfoListnull275     private fun List<AppItem>.toAppOrderInfoList(): List<AppOrderInfo> {
276         return map { AppOrderInfo(it.packageName, it.className, it.displayName.toString()) }
277     }
278 }
279