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 @file:Suppress("DEPRECATION") 17 18 package com.android.permissioncontroller.permission.ui.model 19 20 import android.app.Application 21 import android.app.usage.UsageStats 22 import android.content.Intent 23 import android.content.pm.ApplicationInfo 24 import android.content.pm.PackageManager 25 import android.net.Uri 26 import android.os.UserHandle 27 import android.provider.Settings 28 import android.util.Log 29 import androidx.fragment.app.Fragment 30 import androidx.lifecycle.ViewModel 31 import androidx.lifecycle.ViewModelProvider 32 import com.android.permissioncontroller.PermissionControllerStatsLog 33 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKED_APP_INTERACTION 34 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE 35 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKE_FRAGMENT_APP_VIEWED 36 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__NEWER_BUCKET 37 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__OLDER_BUCKET 38 import com.android.permissioncontroller.hibernation.lastTimePackageUsed 39 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData 40 import com.android.permissioncontroller.permission.data.SmartAsyncMediatorLiveData 41 import com.android.permissioncontroller.permission.data.UsageStatsLiveData 42 import com.android.permissioncontroller.permission.data.getUnusedPackages 43 import com.android.permissioncontroller.permission.model.livedatatypes.LightPackageInfo 44 import com.android.permissioncontroller.permission.utils.IPC 45 import com.android.permissioncontroller.permission.utils.Utils 46 import kotlin.time.Duration 47 import kotlin.time.Duration.Companion.days 48 import kotlin.time.Duration.Companion.milliseconds 49 import kotlinx.coroutines.GlobalScope 50 import kotlinx.coroutines.Job 51 import kotlinx.coroutines.launch 52 53 /** 54 * UnusedAppsViewModel for the AutoRevokeFragment. Has a livedata which provides all unused apps, 55 * organized by how long they have been unused. 56 */ 57 class UnusedAppsViewModel(private val app: Application, private val sessionId: Long) : ViewModel() { 58 59 companion object { 60 private val MAX_UNUSED_PERIOD_MILLIS = 61 UnusedPeriod.allPeriods.maxBy(UnusedPeriod::duration).duration.inWholeMilliseconds 62 private val LOG_TAG = AppPermissionViewModel::class.java.simpleName 63 } 64 65 enum class UnusedPeriod(val duration: Duration) { 66 ONE_MONTH(30.days), 67 THREE_MONTHS(90.days), 68 SIX_MONTHS(180.days); 69 70 val months: Int = (duration.inWholeDays / 30).toInt() 71 72 fun isNewlyUnused(): Boolean { 73 return (this == ONE_MONTH) || (this == THREE_MONTHS) 74 } 75 76 companion object { 77 78 val allPeriods: List<UnusedPeriod> = values().toList() 79 80 // Find the longest period shorter than unused time 81 fun findLongestValidPeriod(durationInMs: Long): UnusedPeriod { 82 val duration = durationInMs.milliseconds 83 return UnusedPeriod.allPeriods.findLast { duration > it.duration } 84 ?: UnusedPeriod.allPeriods.first() 85 } 86 } 87 } 88 89 data class UnusedPackageInfo( 90 val packageName: String, 91 val user: UserHandle, 92 val isSystemApp: Boolean, 93 val revokedGroups: Set<String>, 94 ) 95 96 private data class PackageLastUsageTime(val packageName: String, val usageTime: Long) 97 98 val unusedPackageCategoriesLiveData = 99 object : 100 SmartAsyncMediatorLiveData<Map<UnusedPeriod, List<UnusedPackageInfo>>>( 101 alwaysUpdateOnActive = false 102 ) { 103 // Get apps usage stats from the longest interesting period (MAX_UNUSED_PERIOD_MILLIS) 104 private val usageStatsLiveData = UsageStatsLiveData[MAX_UNUSED_PERIOD_MILLIS] 105 106 init { 107 addSource(getUnusedPackages()) { onUpdate() } 108 109 addSource(AllPackageInfosLiveData) { onUpdate() } 110 111 addSource(usageStatsLiveData) { onUpdate() } 112 } 113 114 override suspend fun loadDataAndPostValue(job: Job) { 115 if ( 116 !getUnusedPackages().isInitialized || 117 !usageStatsLiveData.isInitialized || 118 !AllPackageInfosLiveData.isInitialized 119 ) { 120 return 121 } 122 123 val unusedApps = getUnusedPackages().value!! 124 Log.i(LOG_TAG, "Unused apps: $unusedApps") 125 val categorizedApps = mutableMapOf<UnusedPeriod, MutableList<UnusedPackageInfo>>() 126 for (period in UnusedPeriod.allPeriods) { 127 categorizedApps[period] = mutableListOf() 128 } 129 130 // Get all packages which cannot be uninstalled. 131 val systemApps = getUnusedSystemApps(AllPackageInfosLiveData.value!!, unusedApps) 132 val lastUsedDataUnusedApps = 133 extractUnusedAppsUsageData(usageStatsLiveData.value!!, unusedApps) { 134 it: UsageStats -> 135 PackageLastUsageTime(it.packageName, it.lastTimePackageUsed()) 136 } 137 val firstInstallDataUnusedApps = 138 extractUnusedAppsUsageData(AllPackageInfosLiveData.value!!, unusedApps) { 139 it: LightPackageInfo -> 140 PackageLastUsageTime(it.packageName, it.firstInstallTime) 141 } 142 143 val now = System.currentTimeMillis() 144 unusedApps.keys.forEach { (packageName, user) -> 145 val userPackage = packageName to user 146 147 // If we didn't find the stat for a package in our usageStats search, it is more 148 // than 149 // 6 months old, or the app has never been opened. Then use first install date 150 // instead. 151 var lastUsageTime = 152 lastUsedDataUnusedApps[userPackage] 153 ?: firstInstallDataUnusedApps[userPackage] ?: 0L 154 155 val period = UnusedPeriod.findLongestValidPeriod(now - lastUsageTime) 156 categorizedApps[period]!!.add( 157 UnusedPackageInfo( 158 packageName, 159 user, 160 systemApps.contains(userPackage), 161 unusedApps[userPackage]!! 162 ) 163 ) 164 } 165 166 postValue(categorizedApps) 167 } 168 } 169 170 // Extract UserPackage information for unused system apps from source map. 171 private fun getUnusedSystemApps( 172 userPackages: Map<UserHandle, List<LightPackageInfo>>, 173 unusedApps: Map<UserPackage, Set<String>>, 174 ): List<UserPackage> { 175 return userPackages 176 .flatMap { (userHandle, packageList) -> 177 packageList 178 .filter { (it.appFlags and ApplicationInfo.FLAG_SYSTEM) != 0 } 179 .map { it.packageName to userHandle } 180 } 181 .filter { unusedApps.contains(it) } 182 } 183 184 /** 185 * Extract PackageLastUsageTime for unused apps from userPackages map. This method may be used 186 * for extracting different usage time (such as installation time or last opened time) from 187 * different Package structures 188 */ 189 private fun <PackageData> extractUnusedAppsUsageData( 190 userPackages: Map<UserHandle, List<PackageData>>, 191 unusedApps: Map<UserPackage, Set<String>>, 192 extractUsageData: (fullData: PackageData) -> PackageLastUsageTime, 193 ): Map<UserPackage, Long> { 194 return userPackages 195 .flatMap { (userHandle, fullData) -> 196 fullData.map { userHandle to extractUsageData(it) } 197 } 198 .associate { (handle, appData) -> (appData.packageName to handle) to appData.usageTime } 199 .filterKeys { unusedApps.contains(it) } 200 } 201 202 fun navigateToAppInfo(packageName: String, user: UserHandle, sessionId: Long) { 203 val userContext = Utils.getUserContext(app, user) 204 val packageUri = Uri.parse("package:$packageName") 205 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageUri) 206 intent.putExtra(Intent.ACTION_AUTO_REVOKE_PERMISSIONS, sessionId) 207 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 208 userContext.startActivityAsUser(intent, user) 209 } 210 211 fun requestUninstallApp(fragment: Fragment, packageName: String, user: UserHandle) { 212 Log.i(LOG_TAG, "sessionId: $sessionId, Requesting uninstall of $packageName, $user") 213 logAppInteraction(packageName, user, AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE) 214 val packageUri = Uri.parse("package:$packageName") 215 val uninstallIntent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri) 216 uninstallIntent.putExtra(Intent.EXTRA_USER, user) 217 fragment.startActivity(uninstallIntent) 218 } 219 220 fun disableApp(packageName: String, user: UserHandle) { 221 Log.i(LOG_TAG, "sessionId: $sessionId, Disabling $packageName, $user") 222 logAppInteraction(packageName, user, AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE) 223 val userContext = Utils.getUserContext(app, user) 224 userContext.packageManager.setApplicationEnabledSetting( 225 packageName, 226 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 227 0 228 ) 229 } 230 231 private fun logAppInteraction(packageName: String, user: UserHandle, action: Int) { 232 GlobalScope.launch(IPC) { 233 // If we are logging an app interaction, then the AllPackageInfosLiveData is not stale. 234 val uid = 235 AllPackageInfosLiveData.value 236 ?.get(user) 237 ?.find { info -> info.packageName == packageName } 238 ?.uid 239 240 if (uid != null) { 241 PermissionControllerStatsLog.write( 242 AUTO_REVOKED_APP_INTERACTION, 243 sessionId, 244 uid, 245 packageName, 246 action 247 ) 248 } 249 } 250 } 251 252 fun logAppView(packageName: String, user: UserHandle, groupName: String, isNew: Boolean) { 253 GlobalScope.launch(IPC) { 254 val uid = 255 AllPackageInfosLiveData.value!![user]!!.find { info -> 256 info.packageName == packageName 257 } 258 ?.uid 259 260 if (uid != null) { 261 val bucket = 262 if (isNew) { 263 AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__NEWER_BUCKET 264 } else { 265 AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__OLDER_BUCKET 266 } 267 PermissionControllerStatsLog.write( 268 AUTO_REVOKE_FRAGMENT_APP_VIEWED, 269 sessionId, 270 uid, 271 packageName, 272 groupName, 273 bucket 274 ) 275 } 276 } 277 } 278 } 279 280 typealias UserPackage = Pair<String, UserHandle> 281 282 class UnusedAppsViewModelFactory( 283 private val app: Application, 284 private val sessionId: Long, 285 ) : ViewModelProvider.Factory { 286 createnull287 override fun <T : ViewModel> create(modelClass: Class<T>): T { 288 @Suppress("UNCHECKED_CAST") return UnusedAppsViewModel(app, sessionId) as T 289 } 290 } 291