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