1 /*
2  * 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.AppOpsManager
21 import android.app.AppOpsManager.MODE_ALLOWED
22 import android.app.AppOpsManager.MODE_IGNORED
23 import android.app.AppOpsManager.OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
24 import android.apphibernation.AppHibernationManager
25 import android.content.Context
26 import android.os.Bundle
27 import android.os.UserHandle
28 import android.util.Log
29 import androidx.fragment.app.Fragment
30 import androidx.lifecycle.ViewModel
31 import androidx.lifecycle.ViewModelProvider
32 import androidx.navigation.fragment.findNavController
33 import com.android.permissioncontroller.PermissionControllerApplication
34 import com.android.permissioncontroller.PermissionControllerStatsLog
35 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION
36 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED
37 import com.android.permissioncontroller.PermissionControllerStatsLog.APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED
38 import com.android.permissioncontroller.R
39 import com.android.permissioncontroller.hibernation.isHibernationEnabled
40 import com.android.permissioncontroller.permission.data.AppPermGroupUiInfoLiveData
41 import com.android.permissioncontroller.permission.data.FullStoragePermissionAppsLiveData
42 import com.android.permissioncontroller.permission.data.HibernationSettingStateLiveData
43 import com.android.permissioncontroller.permission.data.LightPackageInfoLiveData
44 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData
45 import com.android.permissioncontroller.permission.data.PackagePermissionsLiveData.Companion.NON_RUNTIME_NORMAL_PERMS
46 import com.android.permissioncontroller.permission.data.SmartUpdateMediatorLiveData
47 import com.android.permissioncontroller.permission.data.get
48 import com.android.permissioncontroller.permission.model.AppPermissionUsage
49 import com.android.permissioncontroller.permission.model.livedatatypes.AppPermGroupUiInfo.PermGrantState
50 import com.android.permissioncontroller.permission.ui.Category
51 import com.android.permissioncontroller.permission.utils.IPC
52 import com.android.permissioncontroller.permission.utils.Utils
53 import com.android.permissioncontroller.permission.utils.Utils.AppPermsLastAccessType
54 import com.android.permissioncontroller.permission.utils.navigateSafe
55 import kotlinx.coroutines.GlobalScope
56 import kotlinx.coroutines.launch
57 import java.time.Instant
58 import java.util.concurrent.TimeUnit
59 import kotlin.math.max
60 
61 /**
62  * ViewModel for the AppPermissionGroupsFragment. Has a liveData with the UI information for all
63  * permission groups that this package requests runtime permissions from
64  *
65  * @param packageName The name of the package this viewModel is representing
66  * @param user The user of the package this viewModel is representing
67  */
68 class AppPermissionGroupsViewModel(
69     private val packageName: String,
70     private val user: UserHandle,
71     private val sessionId: Long
72 ) : ViewModel() {
73 
74     companion object {
75         const val AGGREGATE_DATA_FILTER_BEGIN_DAYS = 1
76         val LOG_TAG: String = AppPermissionGroupsViewModel::class.java.simpleName
77     }
78 
79     val app = PermissionControllerApplication.get()!!
80 
81     enum class PermSubtitle(val value: Int) {
82         NONE(0),
83         MEDIA_ONLY(1),
84         ALL_FILES(2),
85         FOREGROUND_ONLY(3),
86         BACKGROUND(4),
87     }
88 
89     data class GroupUiInfo(
90         val groupName: String,
91         val isSystem: Boolean = false,
92         val subtitle: PermSubtitle
93     ) {
94         constructor(groupName: String, isSystem: Boolean) :
95             this(groupName, isSystem, PermSubtitle.NONE)
96     }
97 
98     // Auto-revoke and hibernation share the same settings
99     val autoRevokeLiveData = HibernationSettingStateLiveData[packageName, user]
100 
101     /**
102      * LiveData whose data is a map of grant category (either allowed or denied) to a list
103      * of permission group names that match the key, and two booleans representing if this is a
104      * system group, and a subtitle resource ID, if applicable.
105      */
106     val packagePermGroupsLiveData = object : SmartUpdateMediatorLiveData<@JvmSuppressWildcards
107     Map<Category, List<GroupUiInfo>>>() {
108 
109         private val packagePermsLiveData =
110             PackagePermissionsLiveData[packageName, user]
111         private val appPermGroupUiInfoLiveDatas = mutableMapOf<String, AppPermGroupUiInfoLiveData>()
112         private val fullStoragePermsLiveData = FullStoragePermissionAppsLiveData
113 
114         init {
115             addSource(packagePermsLiveData) {
116                 update()
117             }
118             addSource(fullStoragePermsLiveData) {
119                 update()
120             }
121             addSource(autoRevokeLiveData) {
122                 removeSource(autoRevokeLiveData)
123                 update()
124             }
125             update()
126         }
127 
128         override fun onUpdate() {
129             val groups = packagePermsLiveData.value?.keys?.filter { it != NON_RUNTIME_NORMAL_PERMS }
130             if (groups == null && packagePermsLiveData.isInitialized) {
131                 value = null
132                 return
133             } else if (groups == null || (Manifest.permission_group.STORAGE in groups &&
134                     !fullStoragePermsLiveData.isInitialized) || !autoRevokeLiveData.isInitialized) {
135                 return
136             }
137 
138             val getLiveData = { groupName: String ->
139                 AppPermGroupUiInfoLiveData[packageName, groupName, user]
140             }
141             setSourcesToDifference(groups, appPermGroupUiInfoLiveDatas, getLiveData)
142 
143             if (!appPermGroupUiInfoLiveDatas.all { it.value.isInitialized }) {
144                 return
145             }
146 
147             val groupGrantStates = mutableMapOf<Category,
148                 MutableList<GroupUiInfo>>()
149             groupGrantStates[Category.ALLOWED] = mutableListOf()
150             groupGrantStates[Category.ASK] = mutableListOf()
151             groupGrantStates[Category.DENIED] = mutableListOf()
152 
153             val fullStorageState = fullStoragePermsLiveData.value?.find { pkg ->
154                 pkg.packageName == packageName && pkg.user == user
155             }
156 
157             for (groupName in groups) {
158                 val isSystem = Utils.getPlatformPermissionGroups().contains(groupName)
159                 appPermGroupUiInfoLiveDatas[groupName]?.value?.let { uiInfo ->
160                     if (groupName == Manifest.permission_group.STORAGE &&
161                         (fullStorageState?.isGranted == true && !fullStorageState.isLegacy)) {
162                         groupGrantStates[Category.ALLOWED]!!.add(
163                             GroupUiInfo(groupName, isSystem, PermSubtitle.ALL_FILES))
164                         return@let
165                     }
166                     when (uiInfo.permGrantState) {
167                         PermGrantState.PERMS_ALLOWED -> {
168                             val subtitle = if (groupName == Manifest.permission_group.STORAGE) {
169                                 if (fullStorageState?.isLegacy == true) {
170                                     PermSubtitle.ALL_FILES
171                                 } else {
172                                     PermSubtitle.MEDIA_ONLY
173                                 }
174                             } else {
175                                 PermSubtitle.NONE
176                             }
177                             groupGrantStates[Category.ALLOWED]!!.add(
178                                 GroupUiInfo(groupName, isSystem, subtitle))
179                         }
180                         PermGrantState.PERMS_ALLOWED_ALWAYS -> groupGrantStates[
181                             Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem,
182                                 PermSubtitle.BACKGROUND))
183                         PermGrantState.PERMS_ALLOWED_FOREGROUND_ONLY -> groupGrantStates[
184                             Category.ALLOWED]!!.add(GroupUiInfo(groupName, isSystem,
185                                 PermSubtitle.FOREGROUND_ONLY))
186                         PermGrantState.PERMS_DENIED -> groupGrantStates[Category.DENIED]!!.add(
187                             GroupUiInfo(groupName, isSystem))
188                         PermGrantState.PERMS_ASK -> groupGrantStates[Category.ASK]!!.add(
189                             GroupUiInfo(groupName, isSystem))
190                     }
191                 }
192             }
193 
194             value = groupGrantStates
195         }
196     }
197 
198     fun setAutoRevoke(enabled: Boolean) {
199         GlobalScope.launch(IPC) {
200             val aom = app.getSystemService(AppOpsManager::class.java)!!
201             val uid = LightPackageInfoLiveData[packageName, user].getInitializedValue()?.uid
202 
203             if (uid != null) {
204                 Log.i(LOG_TAG, "sessionId $sessionId setting auto revoke enabled to $enabled for" +
205                     "$packageName $user")
206                 val tag = if (enabled) {
207                     APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_ENABLED
208                 } else {
209                     APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION__ACTION__SWITCH_DISABLED
210                 }
211                 PermissionControllerStatsLog.write(
212                     APP_PERMISSION_GROUPS_FRAGMENT_AUTO_REVOKE_ACTION, sessionId, uid, packageName,
213                     tag)
214 
215                 val mode = if (enabled) {
216                     MODE_ALLOWED
217                 } else {
218                     MODE_IGNORED
219                 }
220                 aom.setUidMode(OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, uid, mode)
221                 if (isHibernationEnabled() && !enabled) {
222                     val ahm = app.getSystemService(AppHibernationManager::class.java)!!
223                     ahm.setHibernatingForUser(packageName, false)
224                     ahm.setHibernatingGlobally(packageName, false)
225                 }
226             }
227         }
228     }
229 
230     fun showExtraPerms(fragment: Fragment, args: Bundle) {
231         fragment.findNavController().navigateSafe(R.id.perm_groups_to_custom, args)
232     }
233 
234     fun showAllPermissions(fragment: Fragment, args: Bundle) {
235         fragment.findNavController().navigateSafe(R.id.perm_groups_to_all_perms, args)
236     }
237 
238     // This method should be consolidated with
239     // PermissionAppsViewModel#extractGroupUsageLastAccessTime
240     fun extractGroupUsageLastAccessTime(
241         accessTime: MutableMap<String, Long>,
242         appPermissionUsages: List<AppPermissionUsage>,
243         packageName: String
244     ) {
245         accessTime.clear()
246         val filterTimeBeginMillis = max(System.currentTimeMillis() -
247                 TimeUnit.DAYS.toMillis(AGGREGATE_DATA_FILTER_BEGIN_DAYS.toLong()),
248                 Instant.EPOCH.toEpochMilli())
249         val numApps: Int = appPermissionUsages.size
250         for (appIndex in 0 until numApps) {
251             val appUsage: AppPermissionUsage = appPermissionUsages[appIndex]
252             if (appUsage.packageName != packageName) {
253                 continue
254             }
255             val appGroups = appUsage.groupUsages
256             val numGroups = appGroups.size
257             for (groupIndex in 0 until numGroups) {
258                 val groupUsage = appGroups[groupIndex]
259                 var lastAccessTime = groupUsage.lastAccessTime
260                 val groupName = groupUsage.group.name
261                 if (lastAccessTime == 0L || lastAccessTime < filterTimeBeginMillis) {
262                     continue
263                 }
264 
265                 // We might have another AppPermissionUsage entry that's of the same packageName
266                 // but with a different uid. In that case, we want to grab the max lastAccessTime
267                 // as the last usage to show.
268                 lastAccessTime = Math.max(
269                         accessTime.getOrDefault(groupName, Instant.EPOCH.toEpochMilli()),
270                         lastAccessTime)
271                 accessTime[groupName] = lastAccessTime
272             }
273         }
274     }
275 
276     fun getPreferenceSummary(groupInfo: GroupUiInfo, context: Context, lastAccessTime: Long?):
277             String {
278         val summaryTimestamp = Utils
279                 .getPermissionLastAccessSummaryTimestamp(
280                         lastAccessTime, context, groupInfo.groupName)
281         @AppPermsLastAccessType val lastAccessType: Int = summaryTimestamp.second
282 
283         return when (groupInfo.subtitle) {
284             PermSubtitle.BACKGROUND ->
285                 when (lastAccessType) {
286                     Utils.LAST_24H_CONTENT_PROVIDER -> context.getString(
287                             R.string.app_perms_content_provider_background)
288                     Utils.LAST_24H_SENSOR_TODAY -> context.getString(
289                             R.string.app_perms_24h_access_background,
290                             summaryTimestamp.first)
291                     Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString(
292                             R.string.app_perms_24h_access_yest_background,
293                             summaryTimestamp.first)
294                     Utils.NOT_IN_LAST_24H -> context.getString(
295                             R.string.permission_subtitle_background)
296                     else -> context.getString(
297                             R.string.permission_subtitle_background)
298                 }
299             PermSubtitle.MEDIA_ONLY ->
300                 when (lastAccessType) {
301                     Utils.LAST_24H_CONTENT_PROVIDER -> context.getString(
302                             R.string.app_perms_content_provider_media_only)
303                     Utils.LAST_24H_SENSOR_TODAY -> context.getString(
304                             R.string.app_perms_24h_access_media_only,
305                             summaryTimestamp.first)
306                     Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString(
307                             R.string.app_perms_24h_access_yest_media_only,
308                             summaryTimestamp.first)
309                     Utils.NOT_IN_LAST_24H -> context.getString(
310                             R.string.permission_subtitle_media_only)
311                     else -> context.getString(R.string.permission_subtitle_media_only)
312                 }
313             PermSubtitle.ALL_FILES ->
314                 when (lastAccessType) {
315                     Utils.LAST_24H_CONTENT_PROVIDER -> context.getString(
316                             R.string.app_perms_content_provider_all_files)
317                     Utils.LAST_24H_SENSOR_TODAY -> context.getString(
318                             R.string.app_perms_24h_access_all_files,
319                             summaryTimestamp.first)
320                     Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString(
321                             R.string.app_perms_24h_access_yest_all_files,
322                             summaryTimestamp.first)
323                     Utils.NOT_IN_LAST_24H -> context.getString(
324                             R.string.permission_subtitle_all_files)
325                     else -> context.getString(R.string.permission_subtitle_all_files)
326                 }
327             PermSubtitle.FOREGROUND_ONLY ->
328                 when (lastAccessType) {
329                     Utils.LAST_24H_CONTENT_PROVIDER -> context.getString(
330                             R.string.app_perms_content_provider)
331                     Utils.LAST_24H_SENSOR_TODAY -> context.getString(
332                             R.string.app_perms_24h_access,
333                             summaryTimestamp.first)
334                     Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString(
335                             R.string.app_perms_24h_access_yest,
336                             summaryTimestamp.first)
337                     Utils.NOT_IN_LAST_24H -> ""
338                     else -> ""
339                 }
340             else ->
341                 when (lastAccessType) {
342                     Utils.LAST_24H_CONTENT_PROVIDER -> context.getString(
343                             R.string.app_perms_content_provider)
344                     Utils.LAST_24H_SENSOR_TODAY -> context.getString(
345                             R.string.app_perms_24h_access,
346                             summaryTimestamp.first)
347                     Utils.LAST_24H_SENSOR_YESTERDAY -> context.getString(
348                             R.string.app_perms_24h_access_yest,
349                             summaryTimestamp.first)
350                     Utils.NOT_IN_LAST_24H -> ""
351                     else -> ""
352                 }
353         }
354     }
355 }
356 
357 /**
358  * Factory for an AppPermissionGroupsViewModel
359  *
360  * @param packageName The name of the package this viewModel is representing
361  * @param user The user of the package this viewModel is representing
362  */
363 class AppPermissionGroupsViewModelFactory(
364     private val packageName: String,
365     private val user: UserHandle,
366     private val sessionId: Long
367 ) : ViewModelProvider.Factory {
368 
369     override fun <T : ViewModel> create(modelClass: Class<T>): T {
370         @Suppress("UNCHECKED_CAST")
371         return AppPermissionGroupsViewModel(packageName, user, sessionId) as T
372     }
373 }
374