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 }