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 17 package com.android.permissioncontroller.permission.ui.model 18 19 import android.app.Application 20 import android.content.Intent 21 import android.content.pm.ApplicationInfo 22 import android.content.pm.PackageManager 23 import android.net.Uri 24 import android.os.UserHandle 25 import android.provider.Settings 26 import android.util.Log 27 import androidx.fragment.app.Fragment 28 import androidx.lifecycle.ViewModel 29 import androidx.lifecycle.ViewModelProvider 30 import com.android.permissioncontroller.PermissionControllerStatsLog 31 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKED_APP_INTERACTION 32 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE 33 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKE_FRAGMENT_APP_VIEWED 34 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__NEWER_BUCKET 35 import com.android.permissioncontroller.PermissionControllerStatsLog.AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__OLDER_BUCKET 36 import com.android.permissioncontroller.permission.data.AllPackageInfosLiveData 37 import com.android.permissioncontroller.permission.data.SmartAsyncMediatorLiveData 38 import com.android.permissioncontroller.permission.data.UsageStatsLiveData 39 import com.android.permissioncontroller.permission.data.getUnusedPackages 40 import com.android.permissioncontroller.permission.utils.IPC 41 import com.android.permissioncontroller.permission.utils.Utils 42 import kotlinx.coroutines.GlobalScope 43 import kotlinx.coroutines.Job 44 import kotlinx.coroutines.launch 45 import java.util.concurrent.TimeUnit.DAYS 46 47 /** 48 * UnusedAppsViewModel for the AutoRevokeFragment. Has a livedata which provides all unused apps, 49 * organized by how long they have been unused. 50 */ 51 class UnusedAppsViewModel(private val app: Application, private val sessionId: Long) : ViewModel() { 52 53 companion object { 54 private val SIX_MONTHS_MILLIS = DAYS.toMillis(180) 55 private val LOG_TAG = AppPermissionViewModel::class.java.simpleName 56 } 57 58 enum class Months(val value: String) { 59 THREE("three_months"), 60 SIX("six_months"); 61 62 companion object { 63 @JvmStatic 64 fun allMonths(): List<Months> { 65 return listOf(THREE, SIX) 66 } 67 } 68 } 69 70 data class UnusedPackageInfo( 71 val packageName: String, 72 val user: UserHandle, 73 val shouldDisable: Boolean, 74 val revokedGroups: Set<String> 75 ) 76 77 val unusedPackageCategoriesLiveData = object 78 : SmartAsyncMediatorLiveData<Map<Months, List<UnusedPackageInfo>>>( 79 alwaysUpdateOnActive = false 80 ) { 81 private val usageStatsLiveData = UsageStatsLiveData[SIX_MONTHS_MILLIS] 82 83 init { 84 addSource(getUnusedPackages()) { 85 onUpdate() 86 } 87 88 addSource(AllPackageInfosLiveData) { 89 onUpdate() 90 } 91 92 addSource(usageStatsLiveData) { 93 onUpdate() 94 } 95 } 96 97 override suspend fun loadDataAndPostValue(job: Job) { 98 if (!getUnusedPackages().isInitialized || 99 !usageStatsLiveData.isInitialized || !AllPackageInfosLiveData.isInitialized) { 100 return 101 } 102 103 val unusedApps = getUnusedPackages().value!! 104 Log.i(LOG_TAG, "Unused apps: $unusedApps") 105 val overSixMonthApps = unusedApps.keys.toMutableSet() 106 val categorizedApps = mutableMapOf<Months, MutableList<UnusedPackageInfo>>() 107 categorizedApps[Months.THREE] = mutableListOf() 108 categorizedApps[Months.SIX] = mutableListOf() 109 110 // Get all packages which should be disabled, instead of uninstalled 111 val disableActionApps = mutableListOf<Pair<String, UserHandle>>() 112 for ((user, packageList) in AllPackageInfosLiveData.value!!) { 113 disableActionApps.addAll(packageList.mapNotNull { packageInfo -> 114 val key = packageInfo.packageName to user 115 if (unusedApps.contains(key) && 116 (packageInfo.appFlags and ApplicationInfo.FLAG_SYSTEM) != 0) { 117 key 118 } else { 119 null 120 } 121 }) 122 } 123 124 val now = System.currentTimeMillis() 125 for ((user, stats) in usageStatsLiveData.value!!) { 126 for (stat in stats) { 127 val statPackage = stat.packageName to user 128 if (!unusedApps.contains(statPackage)) { 129 continue 130 } 131 132 categorizedApps[Months.THREE]!!.add( 133 UnusedPackageInfo(stat.packageName, user, 134 disableActionApps.contains(statPackage), unusedApps[statPackage]!!)) 135 overSixMonthApps.remove(statPackage) 136 } 137 } 138 139 // If we didn't find the stat for a package in our six month search, it is more than 140 // 6 months old, or the app has never been opened. 141 overSixMonthApps.forEach { (packageName, user) -> 142 var installTime: Long = 0 143 for (pI in AllPackageInfosLiveData.value!![user]!!) { 144 if (pI.packageName == packageName) { 145 installTime = pI.firstInstallTime 146 } 147 } 148 149 // Check if the app was installed less than six months ago, and never opened 150 val months = if (now - installTime <= SIX_MONTHS_MILLIS) { 151 Months.THREE 152 } else { 153 Months.SIX 154 } 155 val userPackage = packageName to user 156 categorizedApps[months]!!.add( 157 UnusedPackageInfo(packageName, user, disableActionApps.contains(userPackage), 158 unusedApps[userPackage]!!)) 159 } 160 161 postValue(categorizedApps) 162 } 163 } 164 165 fun areUnusedPackagesLoaded(): Boolean { 166 return getUnusedPackages().isInitialized 167 } 168 169 fun navigateToAppInfo(packageName: String, user: UserHandle, sessionId: Long) { 170 val userContext = Utils.getUserContext(app, user) 171 val packageUri = Uri.parse("package:$packageName") 172 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageUri) 173 intent.putExtra(Intent.ACTION_AUTO_REVOKE_PERMISSIONS, sessionId) 174 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK 175 userContext.startActivityAsUser(intent, user) 176 } 177 178 fun requestUninstallApp(fragment: Fragment, packageName: String, user: UserHandle) { 179 Log.i(LOG_TAG, "sessionId: $sessionId, Requesting uninstall of $packageName, $user") 180 logAppInteraction(packageName, user, AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE) 181 val packageUri = Uri.parse("package:$packageName") 182 val uninstallIntent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri) 183 uninstallIntent.putExtra(Intent.EXTRA_USER, user) 184 fragment.startActivity(uninstallIntent) 185 } 186 187 fun disableApp(packageName: String, user: UserHandle) { 188 Log.i(LOG_TAG, "sessionId: $sessionId, Disabling $packageName, $user") 189 logAppInteraction(packageName, user, AUTO_REVOKED_APP_INTERACTION__ACTION__REMOVE) 190 val userContext = Utils.getUserContext(app, user) 191 userContext.packageManager.setApplicationEnabledSetting(packageName, 192 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0) 193 } 194 195 private fun logAppInteraction(packageName: String, user: UserHandle, action: Int) { 196 GlobalScope.launch(IPC) { 197 // If we are logging an app interaction, then the AllPackageInfosLiveData is not stale. 198 val uid = AllPackageInfosLiveData.value?.get(user)?.find { 199 info -> info.packageName == packageName }?.uid 200 201 if (uid != null) { 202 PermissionControllerStatsLog.write(AUTO_REVOKED_APP_INTERACTION, sessionId, 203 uid, packageName, action) 204 } 205 } 206 } 207 208 fun logAppView(packageName: String, user: UserHandle, groupName: String, isNew: Boolean) { 209 GlobalScope.launch(IPC) { 210 val uid = AllPackageInfosLiveData.value!![user]!!.find { 211 info -> info.packageName == packageName }?.uid 212 213 if (uid != null) { 214 val bucket = if (isNew) { 215 AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__NEWER_BUCKET 216 } else { 217 AUTO_REVOKE_FRAGMENT_APP_VIEWED__AGE__OLDER_BUCKET 218 } 219 PermissionControllerStatsLog.write(AUTO_REVOKE_FRAGMENT_APP_VIEWED, sessionId, 220 uid, packageName, groupName, bucket) 221 } 222 } 223 } 224 } 225 226 class UnusedAppsViewModelFactory( 227 private val app: Application, 228 private val sessionId: Long 229 ) : ViewModelProvider.Factory { 230 createnull231 override fun <T : ViewModel> create(modelClass: Class<T>): T { 232 @Suppress("UNCHECKED_CAST") 233 return UnusedAppsViewModel(app, sessionId) as T 234 } 235 }