• 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.handheld
18 
19 import android.Manifest.permission_group
20 import android.app.AlertDialog
21 import android.app.Dialog
22 import android.content.Intent
23 import android.os.Bundle
24 import android.os.UserHandle
25 import android.util.Log
26 import android.view.MenuItem
27 import android.view.View
28 import androidx.fragment.app.DialogFragment
29 import androidx.lifecycle.Observer
30 import androidx.lifecycle.ViewModelProvider
31 import androidx.preference.Preference
32 import androidx.preference.PreferenceCategory
33 import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID
34 import com.android.permissioncontroller.Constants.INVALID_SESSION_ID
35 import com.android.permissioncontroller.R
36 import com.android.permissioncontroller.permission.ui.model.AutoRevokeViewModel
37 import com.android.permissioncontroller.permission.ui.model.AutoRevokeViewModel.Months
38 import com.android.permissioncontroller.permission.ui.model.AutoRevokeViewModel.RevokedPackageInfo
39 import com.android.permissioncontroller.permission.ui.model.AutoRevokeViewModelFactory
40 import com.android.permissioncontroller.permission.utils.IPC
41 import com.android.permissioncontroller.permission.utils.KotlinUtils
42 import kotlinx.coroutines.Dispatchers.Main
43 import kotlinx.coroutines.GlobalScope
44 import kotlinx.coroutines.delay
45 import kotlinx.coroutines.launch
46 import java.text.Collator
47 
48 /**
49  * A fragment displaying all applications that have been auto-revoked, as well as the option to
50  * remove them, and to open them.
51  */
52 class AutoRevokeFragment : PermissionsFrameFragment() {
53 
54     private lateinit var viewModel: AutoRevokeViewModel
55     private lateinit var collator: Collator
56     private var sessionId: Long = 0L
57     private var isFirstLoad = false
58 
59     companion object {
60         private const val SHOW_LOAD_DELAY_MS = 200L
61         private const val INFO_MSG_KEY = "info_msg"
62         private const val ELEVATION_HIGH = 8f
63         private val LOG_TAG = AutoRevokeFragment::class.java.simpleName
64 
65         @JvmStatic
66         fun newInstance(): AutoRevokeFragment {
67             return AutoRevokeFragment()
68         }
69 
70         /**
71          * Create the args needed for this fragment
72          *
73          * @param sessionId The current session Id
74          *
75          * @return A bundle containing the session Id
76          */
77         @JvmStatic
78         fun createArgs(sessionId: Long): Bundle {
79             val bundle = Bundle()
80             bundle.putLong(EXTRA_SESSION_ID, sessionId)
81             return bundle
82         }
83     }
84 
85     override fun onCreate(savedInstanceState: Bundle?) {
86         mUseShadowController = false
87         super.onCreate(savedInstanceState)
88         isFirstLoad = true
89 
90         collator = Collator.getInstance(
91             context!!.getResources().getConfiguration().getLocales().get(0))
92         sessionId = arguments!!.getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID)
93         val factory = AutoRevokeViewModelFactory(activity!!.application, sessionId)
94         viewModel = ViewModelProvider(this, factory).get(AutoRevokeViewModel::class.java)
95         viewModel.autoRevokedPackageCategoriesLiveData.observe(this, Observer {
96             it?.let { pkgs ->
97                 updatePackages(pkgs)
98                 setLoading(false, true)
99             }
100         })
101 
102         setHasOptionsMenu(true)
103         activity?.getActionBar()?.setDisplayHomeAsUpEnabled(true)
104 
105         if (!viewModel.areAutoRevokedPackagesLoaded()) {
106             GlobalScope.launch(IPC) {
107                 delay(SHOW_LOAD_DELAY_MS)
108                 if (!viewModel.areAutoRevokedPackagesLoaded()) {
109                     GlobalScope.launch(Main) {
110                         setLoading(true, true)
111                     }
112                 }
113             }
114         }
115     }
116 
117     override fun onStart() {
118         super.onStart()
119         val ab = activity?.actionBar
120         if (ab != null) {
121             ab!!.setElevation(ELEVATION_HIGH)
122         }
123         activity!!.title = getString(R.string.permission_removed_page_title)
124     }
125 
126     override fun onOptionsItemSelected(item: MenuItem): Boolean {
127         if (item.itemId == android.R.id.home) {
128             this.pressBack()
129             return true
130         }
131         return super.onOptionsItemSelected(item)
132     }
133 
134     private fun updatePackages(categorizedPackages: Map<Months, List<RevokedPackageInfo>>) {
135         if (preferenceScreen == null) {
136             addPreferencesFromResource(R.xml.unused_app_categories)
137             val infoPref = preferenceScreen?.findPreference<FooterPreference>(INFO_MSG_KEY)
138             infoPref?.secondSummary = getString(R.string.auto_revoke_open_app_message)
139         }
140 
141         val removedPrefs = mutableMapOf<String, AutoRevokePermissionPreference>()
142         for (month in Months.allMonths()) {
143             val category = findPreference<PreferenceCategory>(month.value)!!
144             for (i in 0 until category.preferenceCount) {
145                 val pref = category.getPreference(i) as AutoRevokePermissionPreference
146                 val contains = categorizedPackages[Months.THREE]?.any { (pkgName, user, _) ->
147                     val key = createKey(pkgName, user)
148                     pref.key == key
149                 }
150                 if (contains != true) {
151                     removedPrefs[pref.key] = pref
152                 }
153             }
154 
155             for ((_, pref) in removedPrefs) {
156                 category.removePreference(pref)
157             }
158         }
159 
160         for ((month, packages) in categorizedPackages) {
161             val category = findPreference<PreferenceCategory>(month.value)!!
162             category.title = if (month == Months.THREE) {
163                 getString(R.string.last_opened_category_title, "3")
164             } else {
165                 getString(R.string.last_opened_category_title, "6")
166             }
167             category.isVisible = packages.isNotEmpty()
168 
169             for ((pkgName, user, shouldDisable, permSet) in packages) {
170                 val revokedPerms = permSet.toList()
171                 val key = createKey(pkgName, user)
172 
173                 var pref = category.findPreference<AutoRevokePermissionPreference>(key)
174                 if (pref == null) {
175                     pref = removedPrefs[key] ?: AutoRevokePermissionPreference(
176                         activity!!.application, pkgName, user, preferenceManager.context!!)
177                     pref.key = key
178                     pref.title = KotlinUtils.getPackageLabel(activity!!.application, pkgName, user)
179                 }
180 
181                 if (shouldDisable) {
182                     pref.removeClickListener = View.OnClickListener {
183                         createDisableDialog(pkgName, user)
184                     }
185                 } else {
186                     pref.removeClickListener = View.OnClickListener {
187                         viewModel.requestUninstallApp(this, pkgName, user)
188                     }
189                 }
190 
191                 pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
192                     viewModel.navigateToAppInfo(pkgName, user, sessionId)
193                     true
194                 }
195 
196                 val mostImportant = getMostImportantGroup(revokedPerms)
197                 val importantLabel = KotlinUtils.getPermGroupLabel(context!!, mostImportant)
198                 pref.summary = when {
199                     revokedPerms.size == 1 -> getString(R.string.auto_revoked_app_summary_one,
200                         importantLabel)
201                     revokedPerms.size == 2 -> {
202                         val otherLabel = if (revokedPerms[0] == mostImportant) {
203                             KotlinUtils.getPermGroupLabel(context!!, revokedPerms[1])
204                         } else {
205                             KotlinUtils.getPermGroupLabel(context!!, revokedPerms[0])
206                         }
207                         getString(R.string.auto_revoked_app_summary_two, importantLabel, otherLabel)
208                     }
209                     else -> getString(R.string.auto_revoked_app_summary_many, importantLabel,
210                         "${revokedPerms.size - 1}")
211                 }
212                 category.addPreference(pref)
213                 KotlinUtils.sortPreferenceGroup(category, this::comparePreference, false)
214             }
215         }
216 
217         if (isFirstLoad) {
218             if (categorizedPackages[Months.SIX]!!.isNotEmpty() ||
219                     categorizedPackages[Months.THREE]!!.isNotEmpty()) {
220                 isFirstLoad = false
221             }
222             Log.i(LOG_TAG, "sessionId: $sessionId Showed Auto Revoke Page")
223             for (month in Months.values()) {
224                 Log.i(LOG_TAG, "sessionId: $sessionId $month unused: " +
225                     "${categorizedPackages[month]}")
226                 for (revokedPackageInfo in categorizedPackages[month]!!) {
227                     for (groupName in revokedPackageInfo.revokedGroups) {
228                         val isNewlyRevoked = month == Months.THREE
229                         viewModel.logAppView(revokedPackageInfo.packageName,
230                             revokedPackageInfo.user, groupName, isNewlyRevoked)
231                     }
232                 }
233             }
234         }
235     }
236 
237     private fun comparePreference(lhs: Preference, rhs: Preference): Int {
238         var result = collator.compare(lhs.title.toString(),
239             rhs.title.toString())
240         if (result == 0) {
241             result = lhs.key.compareTo(rhs.key)
242         }
243         return result
244     }
245 
246     private fun createKey(packageName: String, user: UserHandle): String {
247         return "$packageName:${user.identifier}"
248     }
249 
250     private fun getMostImportantGroup(groupNames: List<String>): String {
251         return when {
252             groupNames.contains(permission_group.LOCATION) -> permission_group.LOCATION
253             groupNames.contains(permission_group.MICROPHONE) -> permission_group.MICROPHONE
254             groupNames.contains(permission_group.CAMERA) -> permission_group.CAMERA
255             groupNames.contains(permission_group.CONTACTS) -> permission_group.CONTACTS
256             groupNames.contains(permission_group.STORAGE) -> permission_group.STORAGE
257             groupNames.contains(permission_group.CALENDAR) -> permission_group.CALENDAR
258             groupNames.isNotEmpty() -> groupNames[0]
259             else -> ""
260         }
261     }
262 
263     private fun createDisableDialog(packageName: String, user: UserHandle) {
264         val dialog = DisableDialog()
265 
266         val args = Bundle()
267         args.putString(Intent.EXTRA_PACKAGE_NAME, packageName)
268         args.putParcelable(Intent.EXTRA_USER, user)
269         dialog.arguments = args
270 
271         dialog.isCancelable = true
272 
273         dialog.show(childFragmentManager.beginTransaction(), DisableDialog::class.java.name)
274     }
275 
276     class DisableDialog : DialogFragment() {
277         override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
278             val fragment = parentFragment as AutoRevokeFragment
279             val packageName = arguments!!.getString(Intent.EXTRA_PACKAGE_NAME)!!
280             val user = arguments!!.getParcelable<UserHandle>(Intent.EXTRA_USER)!!
281             val b = AlertDialog.Builder(context!!)
282                 .setMessage(R.string.app_disable_dlg_text)
283                 .setPositiveButton(R.string.app_disable_dlg_positive) { _, _ ->
284                     fragment.viewModel.disableApp(packageName, user)
285                 }
286                 .setNegativeButton(R.string.cancel, null)
287             val d: Dialog = b.create()
288             d.setCanceledOnTouchOutside(true)
289             return d
290         }
291     }
292 }