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