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