1 /* <lambda>null2 * Copyright (C) 2020 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.permissioncontroller.permission.ui.model 18 19 import android.Manifest 20 import android.app.Application 21 import android.content.Intent 22 import android.os.Bundle 23 import android.os.UserHandle 24 import androidx.fragment.app.Fragment 25 import androidx.lifecycle.AbstractSavedStateViewModelFactory 26 import androidx.lifecycle.MediatorLiveData 27 import androidx.lifecycle.SavedStateHandle 28 import androidx.lifecycle.ViewModel 29 import androidx.navigation.fragment.findNavController 30 import androidx.savedstate.SavedStateRegistryOwner 31 import com.android.permissioncontroller.R 32 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData 33 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData 34 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData.FullStoragePackageState 35 import com.android.permissioncontroller.permission.data.SinglePermGroupPackagesUiInfoLiveData 36 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState 37 import com.android.permissioncontroller.permission.ui.Category 38 import com.android.permissioncontroller.permission.ui.LocationProviderInterceptDialog 39 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.CREATION_LOGGED_KEY 40 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.HAS_SYSTEM_APPS_KEY 41 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOULD_SHOW_SYSTEM_KEY 42 import com.android.permissioncontroller.permission.ui.model.PermissionAppsViewModel.Companion.SHOW_ALWAYS_ALLOWED 43 import com.android.permissioncontroller.permission.utils.LocationUtils 44 import com.android.permissioncontroller.permission.utils.navigateSafe 45 46 /** 47 * ViewModel for the PermissionAppsFragment. Has a liveData with all of the UI info for each 48 * package which requests permissions in this permission group, a liveData which tracks whether or 49 * not to show system apps, and a liveData tracking whether there are any system apps which request 50 * permissions in this group. 51 * 52 * @param app The current application 53 * @param groupName The name of the permission group this viewModel is representing 54 */ 55 class PermissionAppsViewModel( 56 private val state: SavedStateHandle, 57 private val app: Application, 58 private val groupName: String 59 ) : ViewModel() { 60 61 companion object { 62 internal const val SHOULD_SHOW_SYSTEM_KEY = "showSystem" 63 internal const val HAS_SYSTEM_APPS_KEY = "hasSystem" 64 internal const val SHOW_ALWAYS_ALLOWED = "showAlways" 65 internal const val CREATION_LOGGED_KEY = "creationLogged" 66 } 67 68 val shouldShowSystemLiveData = state.getLiveData(SHOULD_SHOW_SYSTEM_KEY, false) 69 val hasSystemAppsLiveData = state.getLiveData(HAS_SYSTEM_APPS_KEY, true) 70 val showAllowAlwaysStringLiveData = state.getLiveData(SHOW_ALWAYS_ALLOWED, false) 71 val categorizedAppsLiveData = CategorizedAppsLiveData(groupName) 72 73 fun updateShowSystem(showSystem: Boolean) { 74 if (showSystem != state.get(SHOULD_SHOW_SYSTEM_KEY)) { 75 state.set(SHOULD_SHOW_SYSTEM_KEY, showSystem) 76 } 77 } 78 79 var creationLogged 80 get() = state.get(CREATION_LOGGED_KEY) ?: false 81 set(value) = state.set(CREATION_LOGGED_KEY, value) 82 83 inner class CategorizedAppsLiveData(groupName: String) 84 : MediatorLiveData<@kotlin.jvm.JvmSuppressWildcards 85 Map<Category, List<Pair<String, UserHandle>>>>() { 86 private val packagesUiInfoLiveData = SinglePermGroupPackagesUiInfoLiveData[groupName] 87 88 init { 89 var fullStorageLiveData: FullStoragePermissionAppsLiveData? = null 90 91 // If this is the Storage group, observe a FullStoragePermissionAppsLiveData, update 92 // the packagesWithFullFileAccess list, and call update to populate the subtitles. 93 if (groupName == Manifest.permission_group.STORAGE) { 94 fullStorageLiveData = FullStoragePermissionAppsLiveData 95 addSource(FullStoragePermissionAppsLiveData) { fullAccessPackages -> 96 if (fullAccessPackages != packagesWithFullFileAccess) { 97 packagesWithFullFileAccess = fullAccessPackages.filter { it.isGranted } 98 if (packagesUiInfoLiveData.isInitialized) { 99 update() 100 } 101 } 102 } 103 } 104 105 addSource(packagesUiInfoLiveData) { 106 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) 107 update() 108 } 109 addSource(shouldShowSystemLiveData) { 110 if (fullStorageLiveData == null || fullStorageLiveData.isInitialized) 111 update() 112 } 113 114 if ((fullStorageLiveData == null || fullStorageLiveData.isInitialized) && 115 packagesUiInfoLiveData.isInitialized) { 116 packagesWithFullFileAccess = fullStorageLiveData?.value?.filter { it.isGranted } 117 ?: emptyList() 118 update() 119 } 120 } 121 122 fun update() { 123 val categoryMap = mutableMapOf<Category, MutableList<Pair<String, UserHandle>>>() 124 val showSystem: Boolean = state.get(SHOULD_SHOW_SYSTEM_KEY) ?: false 125 126 categoryMap[Category.ALLOWED] = mutableListOf() 127 categoryMap[Category.ALLOWED_FOREGROUND] = mutableListOf() 128 categoryMap[Category.ASK] = mutableListOf() 129 categoryMap[Category.DENIED] = mutableListOf() 130 131 val packageMap = packagesUiInfoLiveData.value ?: run { 132 if (packagesUiInfoLiveData.isInitialized) { 133 value = categoryMap 134 } 135 return 136 } 137 138 val hasSystem = packageMap.any { it.value.isSystem && it.value.shouldShow } 139 if (hasSystem != state.get(HAS_SYSTEM_APPS_KEY)) { 140 state.set(HAS_SYSTEM_APPS_KEY, hasSystem) 141 } 142 143 var showAlwaysAllowedString = false 144 145 for ((packageUserPair, uiInfo) in packageMap) { 146 if (!uiInfo.shouldShow) { 147 continue 148 } 149 150 if (uiInfo.isSystem && !showSystem) { 151 continue 152 } 153 154 if (uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_ALWAYS || 155 uiInfo.permGrantState == PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY) { 156 showAlwaysAllowedString = true 157 } 158 159 var category = when (uiInfo.permGrantState) { 160 PermGrantState.PERMS_ALLOWED -> Category.ALLOWED 161 PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> Category.ALLOWED_FOREGROUND 162 PermGrantState.PERMS_ALLOWED_ALWAYS -> Category.ALLOWED 163 PermGrantState.PERMS_DENIED -> Category.DENIED 164 PermGrantState.PERMS_ASK -> Category.ASK 165 } 166 167 if (groupName == Manifest.permission_group.STORAGE && 168 packagesWithFullFileAccess.any { !it.isLegacy && it.isGranted && 169 it.packageName to it.user == packageUserPair }) { 170 category = Category.ALLOWED 171 } 172 categoryMap[category]!!.add(packageUserPair) 173 } 174 showAllowAlwaysStringLiveData.value = showAlwaysAllowedString 175 value = categoryMap 176 } 177 } 178 179 /** 180 * If this is the storage permission group, some apps have full access to storage, while 181 * others just have access to media files. This list contains the packages with full access. 182 * To listen for changes, create and observe a FullStoragePermissionAppsLiveData 183 */ 184 private var packagesWithFullFileAccess = listOf<FullStoragePackageState>() 185 186 /** 187 * Whether or not to show the "Files and Media" subtitle label for a package, vs. the normal 188 * "Media". Requires packagesWithFullFileAccess to be updated in order to work. To do this, 189 * create and observe a FullStoragePermissionAppsLiveData. 190 * 191 * @param packageName The name of the package we want to check 192 * @param user The name of the user whose package we want to check 193 * 194 * @return true if the package and user has full file access 195 */ 196 fun packageHasFullStorage(packageName: String, user: UserHandle): Boolean { 197 return packagesWithFullFileAccess.any { 198 it.packageName == packageName && it.user == user } 199 } 200 201 /** 202 * Whether or not packages have been loaded from the system. 203 * To update, need to observe the allPackageInfosLiveData. 204 * 205 * @return Whether or not all packages have been loaded 206 */ 207 fun arePackagesLoaded(): Boolean { 208 return AllPackageInfosLiveData.isInitialized 209 } 210 211 /** 212 * Navigate to an AppPermissionFragment, unless this is a special location package 213 * 214 * @param fragment The fragment attached to this ViewModel 215 * @param packageName The package name we want to navigate to 216 * @param user The user we want to navigate to the package of 217 * @param args The arguments to pass onto the fragment 218 */ 219 fun navigateToAppPermission( 220 fragment: Fragment, 221 packageName: String, 222 user: UserHandle, 223 args: Bundle 224 ) { 225 val activity = fragment.activity!! 226 if (LocationUtils.isLocationGroupAndProvider( 227 activity, groupName, packageName)) { 228 val intent = Intent(activity, LocationProviderInterceptDialog::class.java) 229 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 230 activity.startActivityAsUser(intent, user) 231 return 232 } 233 234 if (LocationUtils.isLocationGroupAndControllerExtraPackage( 235 activity, groupName, packageName)) { 236 // Redirect to location controller extra package settings. 237 LocationUtils.startLocationControllerExtraPackageSettings(activity, user) 238 return 239 } 240 241 fragment.findNavController().navigateSafe(R.id.perm_apps_to_app, args) 242 } 243 } 244 245 /** 246 * Factory for a PermissionAppsViewModel 247 * 248 * @param app The current application of the fragment 249 * @param groupName The name of the permission group this viewModel is representing 250 * @param owner The owner of this saved state 251 * @param defaultArgs The default args to pass 252 */ 253 class PermissionAppsViewModelFactory( 254 private val app: Application, 255 private val groupName: String, 256 owner: SavedStateRegistryOwner, 257 defaultArgs: Bundle 258 ) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { 259 createnull260 override fun <T : ViewModel?> create(p0: String, p1: Class<T>, state: SavedStateHandle): T { 261 state.set(SHOULD_SHOW_SYSTEM_KEY, state.get<Boolean>(SHOULD_SHOW_SYSTEM_KEY) ?: false) 262 state.set(HAS_SYSTEM_APPS_KEY, state.get<Boolean>(HAS_SYSTEM_APPS_KEY) ?: true) 263 state.set(SHOW_ALWAYS_ALLOWED, state.get<Boolean>(SHOW_ALWAYS_ALLOWED) ?: false) 264 state.set(CREATION_LOGGED_KEY, state.get<Boolean>(CREATION_LOGGED_KEY) ?: false) 265 @Suppress("UNCHECKED_CAST") 266 return PermissionAppsViewModel(state, app, groupName) as T 267 } 268 }