1 /* 2 * Copyright (C) 2022 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.v33 18 19 import android.app.Application 20 import android.content.Intent 21 import android.graphics.drawable.Drawable 22 import android.icu.lang.UCharacter 23 import android.os.Build 24 import android.os.UserHandle 25 import android.text.BidiFormatter 26 import androidx.annotation.RequiresApi 27 import androidx.lifecycle.ViewModel 28 import androidx.lifecycle.ViewModelProvider 29 import com.android.permissioncontroller.DumpableLog 30 import com.android.permissioncontroller.R 31 import com.android.permissioncontroller.permission.data.SmartAsyncMediatorLiveData 32 import com.android.permissioncontroller.permission.data.UserPackageInfosLiveData 33 import com.android.permissioncontroller.permission.data.v33.PermissionDecision 34 import com.android.permissioncontroller.permission.data.v33.RecentPermissionDecisionsLiveData 35 import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo 36 import com.android.permissioncontroller.permission.ui.ManagePermissionsActivity 37 import com.android.permissioncontroller.permission.ui.auto.AutoReviewPermissionDecisionsFragment 38 import com.android.permissioncontroller.permission.utils.KotlinUtils 39 import com.android.permissioncontroller.permission.utils.PermissionMapping 40 import com.android.permissioncontroller.permission.utils.StringUtils 41 import java.util.concurrent.TimeUnit 42 import kotlinx.coroutines.Job 43 44 /** Viewmodel for [ReviewPermissionDecisionsFragment] */ 45 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 46 class ReviewPermissionDecisionsViewModel(val app: Application, val user: UserHandle) : ViewModel() { 47 48 val LOG_TAG = "ReviewPermissionDecisionsViewModel" 49 50 private val recentPermissionsLiveData = RecentPermissionDecisionsLiveData() 51 private val userPackageInfosLiveData = UserPackageInfosLiveData[user] 52 53 val recentPermissionDecisionsLiveData = 54 object : 55 SmartAsyncMediatorLiveData<List<PermissionDecision>>(alwaysUpdateOnActive = false) { 56 57 init { <lambda>null58 addSource(recentPermissionsLiveData) { onUpdate() } 59 <lambda>null60 addSource(userPackageInfosLiveData) { onUpdate() } 61 } 62 loadDataAndPostValuenull63 override suspend fun loadDataAndPostValue(job: Job) { 64 if ( 65 !recentPermissionsLiveData.isInitialized || 66 !userPackageInfosLiveData.isInitialized 67 ) { 68 return 69 } 70 71 // create package info lookup map for performance 72 val packageToLightPackageInfo: MutableMap<String, LightPackageInfo> = mutableMapOf() 73 for (lightPackageInfo in userPackageInfosLiveData.value!!) { 74 packageToLightPackageInfo[lightPackageInfo.packageName] = lightPackageInfo 75 } 76 77 // verify that permission state is still correct. Will also filter out any apps that 78 // were uninstalled 79 val decisionsToReview: MutableList<PermissionDecision> = mutableListOf() 80 for (recentDecision in recentPermissionsLiveData.value!!) { 81 val lightPackageInfo = packageToLightPackageInfo[recentDecision.packageName] 82 if (lightPackageInfo == null) { 83 DumpableLog.e( 84 LOG_TAG, 85 "Package $recentDecision.packageName " + "is no longer installed" 86 ) 87 continue 88 } 89 val grantedGroups: List<String?> = 90 lightPackageInfo.grantedPermissions.map { 91 PermissionMapping.getGroupOfPermission( 92 app.packageManager.getPermissionInfo(it, /* flags= */ 0) 93 ) 94 } 95 val currentlyGranted = 96 grantedGroups.contains(recentDecision.permissionGroupName) 97 if (currentlyGranted && recentDecision.isGranted) { 98 decisionsToReview.add(recentDecision) 99 } else if (!currentlyGranted && !recentDecision.isGranted) { 100 decisionsToReview.add(recentDecision) 101 } else { 102 // It's okay for this to happen - the state could change due to role 103 // changes, 104 // app hibernation, or other non-user-driven actions. 105 DumpableLog.d( 106 LOG_TAG, 107 "Permission decision grant state (${recentDecision.isGranted}) " + 108 "for ${recentDecision.packageName} access to " + 109 "${recentDecision.permissionGroupName} does not match current " + 110 "grant state $currentlyGranted" 111 ) 112 } 113 } 114 115 postValue(decisionsToReview) 116 } 117 } 118 getAppIconnull119 fun getAppIcon(packageName: String): Drawable? { 120 return KotlinUtils.getBadgedPackageIcon(app, packageName, user) 121 } 122 createPreferenceTitlenull123 fun createPreferenceTitle(permissionDecision: PermissionDecision): String { 124 val packageLabel = 125 BidiFormatter.getInstance() 126 .unicodeWrap(KotlinUtils.getPackageLabel(app, permissionDecision.packageName, user)) 127 val permissionGroupLabel = 128 KotlinUtils.getPermGroupLabel(app, permissionDecision.permissionGroupName).toString() 129 return if (permissionDecision.isGranted) { 130 app.getString( 131 R.string.granted_permission_decision, 132 packageLabel, 133 UCharacter.toLowerCase(permissionGroupLabel) 134 ) 135 } else { 136 app.getString( 137 R.string.denied_permission_decision, 138 packageLabel, 139 UCharacter.toLowerCase(permissionGroupLabel) 140 ) 141 } 142 } 143 createManageAppPermissionIntentnull144 fun createManageAppPermissionIntent(permissionDecision: PermissionDecision): Intent { 145 return Intent(Intent.ACTION_MANAGE_APP_PERMISSION).apply { 146 putExtra(Intent.EXTRA_PACKAGE_NAME, permissionDecision.packageName) 147 putExtra(Intent.EXTRA_PERMISSION_NAME, permissionDecision.permissionGroupName) 148 putExtra(Intent.EXTRA_USER, user) 149 putExtra( 150 ManagePermissionsActivity.EXTRA_CALLER_NAME, 151 AutoReviewPermissionDecisionsFragment::class.java.name 152 ) 153 } 154 } 155 createSummaryTextnull156 fun createSummaryText(permissionDecision: PermissionDecision): String { 157 val diff = System.currentTimeMillis() - permissionDecision.eventTime 158 val daysAgo = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS).toInt() 159 return StringUtils.getIcuPluralsString(app, R.string.days_ago, daysAgo) 160 } 161 } 162 163 /** Factory for a [ReviewPermissionDecisionsViewModel] */ 164 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 165 class ReviewPermissionDecisionsViewModelFactory(val app: Application, val user: UserHandle) : 166 ViewModelProvider.Factory { 167 createnull168 override fun <T : ViewModel> create(modelClass: Class<T>): T { 169 @Suppress("UNCHECKED_CAST") return ReviewPermissionDecisionsViewModel(app, user) as T 170 } 171 } 172