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