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.car.settings.CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS
21 import android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED
22 import android.content.ContentResolver
23 import android.content.pm.PackageManager
24 import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
25 import android.content.pm.ResolveInfo
26 import android.database.ContentObserver
27 import android.os.Handler
28 import android.os.Looper
29 import android.provider.Settings
30 import android.util.Log
31 import com.android.car.carlauncher.datasources.restricted.RestrictedAppsUtils.getLauncherActivitiesForRestrictedApps
32 import kotlinx.coroutines.CoroutineDispatcher
33 import kotlinx.coroutines.channels.awaitClose
34 import kotlinx.coroutines.flow.Flow
35 import kotlinx.coroutines.flow.callbackFlow
36 import kotlinx.coroutines.flow.conflate
37 import kotlinx.coroutines.flow.flowOf
38 import kotlinx.coroutines.flow.flowOn
39 
40 /**
41  * DataSource exposes a flow which tracks list of tos(Terms of Service) disabled apps.
42  */
43 interface TosDataSource {
getTosStatenull44     fun getTosState(): Flow<TosState>
45 }
46 
47 data class TosState(
48     val shouldBlockTosApps: Boolean,
49     val restrictedApps: List<ResolveInfo> = emptyList()
50 )
51 
52 /**
53  * Impl of [TosDataSource], to surface all DisabledApps apis.
54  *
55  * All the operations in this class are non blocking.
56  */
57 class TosDataSourceImpl(
58     private val contentResolver: ContentResolver,
59     private val packageManager: PackageManager,
60     private val bgDispatcher: CoroutineDispatcher,
61 ) : TosDataSource {
62 
63     /**
64      * Gets a Flow producer which gets if TOS is accepted by the user and
65      * the current list of tos disabled apps installed for this user and found at
66      * [Settings.Secure.getString] for Key [CarSettings.Secure.KEY_UNACCEPTED_TOS_DISABLED_APPS]
67      * __only if__ [TosState.hasAccepted] is false which is reflected by
68      * [CarSettings.Secure.KEY_USER_TOS_ACCEPTED]
69      *
70      * The Flow also pushes new list if there is any updates in the list of disabled apps.
71      * @return Flow of [TosState]
72      */
73     override fun getTosState(): Flow<TosState> {
74         if (isTosAccepted()) {
75             // Tos is accepted, so we do not need to block the apps.
76             // We can assume the list of blocked apps as empty in this case.
77             return flowOf(
78                 TosState(
79                     false,
80                     emptyList()
81                 )
82             )
83         }
84 
85         return callbackFlow {
86             trySend(
87                 TosState(
88                     shouldBlockTosApps(),
89                     getLauncherActivitiesForRestrictedApps(
90                         packageManager,
91                         contentResolver,
92                         KEY_UNACCEPTED_TOS_DISABLED_APPS,
93                         TOS_DISABLED_APPS_SEPARATOR,
94                         MATCH_DISABLED_COMPONENTS
95                     )
96                 )
97             )
98             if (Looper.myLooper() == null) {
99                 Looper.prepare()
100             }
101 
102             val looper: Looper =
103                 Looper.myLooper().takeIf { it != null } ?: Looper.getMainLooper().also {
104                     Log.d(TAG, "Current thread looper is null, fallback to MainLooper")
105                 }
106             // Tos Accepted Observer
107             val tosStateObserver = object : ContentObserver(Handler(looper)) {
108                 override fun onChange(selfChange: Boolean) {
109                     super.onChange(selfChange)
110                     val isAccepted = isTosAccepted()
111                     val restrictedApps: List<ResolveInfo> = if (isAccepted) {
112                         // We don't need to observe the changes once TOS is accepted
113                         contentResolver.unregisterContentObserver(this)
114                         // if TOS is accepted, we can assume that TOS disabled apps will be empty
115                         emptyList()
116                     } else {
117                         getLauncherActivitiesForRestrictedApps(
118                             packageManager,
119                             contentResolver,
120                             KEY_UNACCEPTED_TOS_DISABLED_APPS,
121                             TOS_DISABLED_APPS_SEPARATOR,
122                             MATCH_DISABLED_COMPONENTS
123                         )
124                     }
125                     trySend(TosState(shouldBlockTosApps(), restrictedApps))
126                 }
127             }
128 
129             contentResolver.registerContentObserver(
130                 Settings.Secure.getUriFor(KEY_UNACCEPTED_TOS_DISABLED_APPS),
131                 false,
132                 tosStateObserver
133             )
134 
135             contentResolver.registerContentObserver(
136                 Settings.Secure.getUriFor(KEY_USER_TOS_ACCEPTED),
137                 false,
138                 tosStateObserver
139             )
140 
141             awaitClose {
142                 // If MainLooper do not quit it. MainLooper always stays alive.
143                 if (looper != Looper.getMainLooper()) {
144                     looper.quitSafely()
145                 }
146                 contentResolver.unregisterContentObserver(tosStateObserver)
147             }
148         }.flowOn(bgDispatcher).conflate()
149     }
150 
151     /**
152      * Check if a user has accepted TOS
153      * @return true if the user has accepted Tos, false otherwise
154      */
155     private fun isTosAccepted(): Boolean {
156         val settingsValue = Settings.Secure.getString(
157             contentResolver,
158             KEY_USER_TOS_ACCEPTED
159         )
160         return settingsValue == TOS_ACCEPTED
161     }
162 
163     /**
164      * Only block the apps to the user when the TOS state is [TOS_NOT_ACCEPTED]
165      *
166      * Note: Even when TOS state is [TOS_UNINITIALIZED] we do not want to block tos apps.
167      */
168     private fun shouldBlockTosApps(): Boolean {
169         val settingsValue = Settings.Secure.getString(
170             contentResolver,
171             KEY_USER_TOS_ACCEPTED
172         )
173         return settingsValue == TOS_NOT_ACCEPTED
174     }
175 
176     companion object {
177         // This value indicates if TOS is in uninitialized state
178         const val TOS_UNINITIALIZED = "0"
179 
180         // This value indicates if TOS has not been accepted by the user
181         const val TOS_NOT_ACCEPTED = "1"
182 
183         // This value indicates if TOS has been accepted by the user
184         const val TOS_ACCEPTED = "2"
185         const val TOS_DISABLED_APPS_SEPARATOR = ","
186 
187         private val TAG = TosDataSourceImpl::class.java.simpleName
188     }
189 }
190