• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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