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 : SmartAsyncMediatorLiveData<Map<UnusedPeriod, List<UnusedPackageInfo>>>( 100 alwaysUpdateOnActive = false 101 ) { 102 // Get apps usage stats from the longest interesting period (MAX_UNUSED_PERIOD_MILLIS) 103 private val usageStatsLiveData = UsageStatsLiveData[MAX_UNUSED_PERIOD_MILLIS] 104 105 init { 106 addSource(getUnusedPackages()) { 107 onUpdate() 108 } 109 110 addSource(AllPackageInfosLiveData) { 111 onUpdate() 112 } 113 114 addSource(usageStatsLiveData) { 115 onUpdate() 116 } 117 } 118 119 override suspend fun loadDataAndPostValue(job: Job) { 120 if (!getUnusedPackages().isInitialized || 121 !usageStatsLiveData.isInitialized || !AllPackageInfosLiveData.isInitialized 122 ) { 123 return 124 } 125 126 val unusedApps = getUnusedPackages().value!! 127 Log.i(LOG_TAG, "Unused apps: $unusedApps") 128 val categorizedApps = mutableMapOf<UnusedPeriod, MutableList<UnusedPackageInfo>>() 129 for (period in UnusedPeriod.allPeriods) { 130 categorizedApps[period] = mutableListOf() 131 } 132 133 // Get all packages which cannot be uninstalled. 134 val systemApps = getUnusedSystemApps(AllPackageInfosLiveData.value!!, unusedApps) 135 val lastUsedDataUnusedApps = 136 extractUnusedAppsUsageData(usageStatsLiveData.value!!, unusedApps) 137 { it: UsageStats -> 138 PackageLastUsageTime(it.packageName, it.lastTimePackageUsed()) 139 } 140 val firstInstallDataUnusedApps = 141 extractUnusedAppsUsageData(AllPackageInfosLiveData.value!!, unusedApps) 142 { it: LightPackageInfo -> 143 PackageLastUsageTime(it.packageName, it.firstInstallTime) 144 } 145 146 val now = System.currentTimeMillis() 147 unusedApps.keys.forEach { (packageName, user) -> 148 val userPackage = packageName to user 149 150 // If we didn't find the stat for a package in our usageStats search, it is more than 151 // 6 months old, or the app has never been opened. Then use first install date instead. 152 var lastUsageTime = 153 lastUsedDataUnusedApps[userPackage] ?: firstInstallDataUnusedApps[ 154 userPackage] ?: 0L 155 156 val period = UnusedPeriod.findLongestValidPeriod(now - lastUsageTime) 157 categorizedApps[period]!!.add( 158 UnusedPackageInfo( 159 packageName, user, systemApps.contains(userPackage), 160 unusedApps[userPackage]!! 161 ) 162 ) 163 } 164 165 postValue(categorizedApps) 166 } 167 } 168 169 // Extract UserPackage information for unused system apps from source map. 170 private fun getUnusedSystemApps( 171 userPackages: Map<UserHandle, List<LightPackageInfo>>, 172 unusedApps: Map<UserPackage, Set<String>>, 173 ): List<UserPackage> { 174 return userPackages.flatMap { (userHandle, packageList) -> 175 packageList.filter { (it.appFlags and ApplicationInfo.FLAG_SYSTEM) != 0 } 176 .map { it.packageName to userHandle } 177 }.filter { unusedApps.contains(it) } 178 } 179 180 /** 181 * Extract PackageLastUsageTime for unused apps from userPackages map. This method may be used 182 * for extracting different usage time (such as installation time or last opened time) from 183 * different Package structures 184 */ 185 private fun <PackageData> extractUnusedAppsUsageData( 186 userPackages: Map<UserHandle, List<PackageData>>, 187 unusedApps: Map<UserPackage, Set<String>>, 188 extractUsageData: (fullData: PackageData) -> PackageLastUsageTime, 189 ): Map<UserPackage, Long> { 190 return userPackages.flatMap { (userHandle, fullData) -> 191 fullData.map { userHandle to extractUsageData(it) } 192 }.associate { (handle, appData) -> (appData.packageName to handle) to appData.usageTime } 193 .filterKeys { unusedApps.contains(it) } 194 } 195 196 fun navigateToAppInfo(packageName: String, user: UserHandle, sessionId: Long) { 197 val userContext = Utils.getUserContext(app, user) 198 val packageUri = Uri.parse("package:$packageName") 199 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageUri) 200 intent.putExtra(Intent.ACTION_AUTO_REVOKE_PERMISSIONS, sessionId) 201 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 202 userContext.startActivityAsUser(intent, user) 203 } 204 205 fun requestUninstallApp(fragment: Fragment, packageName: String, user: UserHandle) { 206 Log.i(LOG_TAG, "sessionId: $sessionId, Requesting uninstall of $packageName, $user") 207 logAppInteraction(packageName, user, AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE) 208 val packageUri = Uri.parse("package:$packageName") 209 val uninstallIntent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri) 210 uninstallIntent.putExtra(Intent.EXTRA_USER, user) 211 fragment.startActivity(uninstallIntent) 212 } 213 214 fun disableApp(packageName: String, user: UserHandle) { 215 Log.i(LOG_TAG, "sessionId: $sessionId, Disabling $packageName, $user") 216 logAppInteraction(packageName, user, AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE) 217 val userContext = Utils.getUserContext(app, user) 218 userContext.packageManager.setApplicationEnabledSetting(packageName, 219 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0) 220 } 221 222 private fun logAppInteraction(packageName: String, user: UserHandle, action: Int) { 223 GlobalScope.launch(IPC) { 224 // If we are logging an app interaction, then the AllPackageInfosLiveData is not stale. 225 val uid = AllPackageInfosLiveData.value?.get(user) 226 ?.find { info -> info.packageName == packageName }?.uid 227 228 if (uid != null) { 229 PermissionControllerStatsLog.write(AUTO_REVOKED_APP_INTERACTION, sessionId, 230 uid, packageName, action) 231 } 232 } 233 } 234 235 fun logAppView(packageName: String, user: UserHandle, groupName: String, isNew: Boolean) { 236 GlobalScope.launch(IPC) { 237 val uid = AllPackageInfosLiveData.value!![user]!!.find { info -> 238 info.packageName == packageName 239 }?.uid 240 241 if (uid != null) { 242 val bucket = if (isNew) { 243 AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__NEWER_BUCKET 244 } else { 245 AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__OLDER_BUCKET 246 } 247 PermissionControllerStatsLog.write(AUTO_REVOKE_FRAGMENT_APP_VIEWED, sessionId, 248 uid, packageName, groupName, bucket) 249 } 250 } 251 } 252 } 253 254 typealias UserPackage = Pair<String, UserHandle> 255 256 class UnusedAppsViewModelFactory( 257 private val app: Application, 258 private val sessionId: Long, 259 ) : ViewModelProvider.Factory { 260 createnull261 override fun <T : ViewModel> create(modelClass: Class<T>): T { 262 @Suppress("UNCHECKED_CAST") 263 return UnusedAppsViewModel(app, sessionId) as T 264 } 265 } 266