1 /*
2  * 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.restricted
18 
19 import android.car.settings.CarSettings
20 import android.content.ContentResolver
21 import android.content.pm.PackageManager
22 import android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
23 import android.content.pm.ResolveInfo
24 import android.database.ContentObserver
25 import android.os.Handler
26 import android.os.Looper
27 import android.provider.Settings
28 import android.util.Log
29 import com.android.car.carlauncher.datasources.restricted.RestrictedAppsUtils.getLauncherActivitiesForRestrictedApps
30 import kotlinx.coroutines.CoroutineDispatcher
31 import kotlinx.coroutines.channels.awaitClose
32 import kotlinx.coroutines.flow.Flow
33 import kotlinx.coroutines.flow.callbackFlow
34 import kotlinx.coroutines.flow.conflate
35 import kotlinx.coroutines.flow.flowOn
36 
37 /**
38  * DataSource exposes a flow which tracks list of disabled apps.
39  */
40 interface DisabledAppsDataSource {
getDisabledAppsnull41     fun getDisabledApps(): Flow<List<ResolveInfo>>
42 }
43 
44 /**
45  * Impl of [DisabledAppsDataSource], to surface all DisabledApps apis.
46  *
47  * All the operations in this class are non blocking.
48  */
49 class DisabledAppsDataSourceImpl(
50     private val contentResolver: ContentResolver,
51     private val packageManager: PackageManager,
52     private val bgDispatcher: CoroutineDispatcher
53 ) : DisabledAppsDataSource {
54 
55     /**
56      * Gets a Flow producer which gets the current list of disabled apps installed for this user
57      * and found at [Settings.Secure.getString]
58      * for Key [CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE].
59      *
60      * The Flow also pushes new list if there is any updates in the list of disabled apps.
61      */
62     override fun getDisabledApps(): Flow<List<ResolveInfo>> {
63         return callbackFlow {
64             trySend(
65                 getLauncherActivitiesForRestrictedApps(
66                     packageManager,
67                     contentResolver,
68                     CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
69                     PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR,
70                     MATCH_DISABLED_UNTIL_USED_COMPONENTS
71                 )
72             )
73             if (Looper.myLooper() == null) {
74                 Looper.prepare()
75             }
76 
77             val looper: Looper =
78                 Looper.myLooper().takeIf { it != null } ?: Looper.getMainLooper().also {
79                     Log.d(TAG, "Current thread looper is null, fallback to MainLooper")
80                 }
81 
82             val disabledAppsObserver = object : ContentObserver(Handler(looper)) {
83                 override fun onChange(selfChange: Boolean) {
84                     super.onChange(selfChange)
85                     trySend(
86                         getLauncherActivitiesForRestrictedApps(
87                             packageManager,
88                             contentResolver,
89                             CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE,
90                             PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR,
91                             MATCH_DISABLED_UNTIL_USED_COMPONENTS
92                         )
93                     )
94                 }
95             }
96 
97             contentResolver.registerContentObserver(
98                 Settings.Secure.getUriFor(
99                     CarSettings.Secure.KEY_PACKAGES_DISABLED_ON_RESOURCE_OVERUSE
100                 ),
101                 false,
102                 disabledAppsObserver
103             )
104 
105             awaitClose {
106                 // If MainLooper do not quit it. MainLooper always stays alive.
107                 if (looper != Looper.getMainLooper()) {
108                     looper.quitSafely()
109                 }
110                 contentResolver.unregisterContentObserver(disabledAppsObserver)
111             }
112         }.flowOn(bgDispatcher).conflate()
113     }
114 
115     companion object {
116         val TAG: String = DisabledAppsDataSourceImpl::class.java.simpleName
117         const val PACKAGES_DISABLED_ON_RESOURCE_OVERUSE_SEPARATOR = ";"
118     }
119 }
120