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