• 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 
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 }