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