• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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
18 
19 import android.Manifest.permission_group
20 import android.app.AlertDialog
21 import android.app.Application
22 import android.app.Dialog
23 import android.content.Intent
24 import android.os.Bundle
25 import android.os.UserHandle
26 import android.util.Log
27 import androidx.fragment.app.DialogFragment
28 import androidx.fragment.app.Fragment
29 import androidx.lifecycle.Observer
30 import androidx.lifecycle.ViewModelProvider
31 import androidx.preference.Preference
32 import androidx.preference.PreferenceCategory
33 import androidx.preference.PreferenceFragmentCompat
34 import androidx.preference.PreferenceScreen
35 import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID
36 import com.android.permissioncontroller.Constants.INVALID_SESSION_ID
37 import com.android.permissioncontroller.R
38 import com.android.permissioncontroller.hibernation.isHibernationEnabled
39 import com.android.permissioncontroller.permission.ui.model.UnusedAppsViewModel
40 import com.android.permissioncontroller.permission.ui.model.UnusedAppsViewModel.Months
41 import com.android.permissioncontroller.permission.ui.model.UnusedAppsViewModel.UnusedPackageInfo
42 import com.android.permissioncontroller.permission.ui.model.UnusedAppsViewModelFactory
43 import com.android.permissioncontroller.permission.utils.IPC
44 import com.android.permissioncontroller.permission.utils.KotlinUtils
45 import kotlinx.coroutines.Dispatchers.Main
46 import kotlinx.coroutines.GlobalScope
47 import kotlinx.coroutines.delay
48 import kotlinx.coroutines.launch
49 import java.text.Collator
50 
51 /**
52  * A fragment displaying all applications that are unused as well as the option to remove them
53  * and to open them.
54  */
55 class UnusedAppsFragment<PF, UnusedAppPref> : Fragment()
56     where PF : PreferenceFragmentCompat, PF : UnusedAppsFragment.Parent<UnusedAppPref>,
57         UnusedAppPref : Preference, UnusedAppPref : RemovablePref {
58 
59     private lateinit var viewModel: UnusedAppsViewModel
60     private lateinit var collator: Collator
61     private var sessionId: Long = 0L
62     private var isFirstLoad = false
63 
64     companion object {
65         public const val INFO_MSG_CATEGORY = "info_msg_category"
66         private const val SHOW_LOAD_DELAY_MS = 200L
67         private const val INFO_MSG_KEY = "info_msg"
68         private const val ELEVATION_HIGH = 8f
69         private val LOG_TAG = UnusedAppsFragment::class.java.simpleName
70 
71         @JvmStatic
72         fun <PF, UnusedAppPref> newInstance(): UnusedAppsFragment<PF, UnusedAppPref>
73             where PF : PreferenceFragmentCompat, PF : UnusedAppsFragment.Parent<UnusedAppPref>,
74                   UnusedAppPref : Preference, UnusedAppPref : RemovablePref {
75             return UnusedAppsFragment()
76         }
77 
78         /**
79          * Create the args needed for this fragment
80          *
81          * @param sessionId The current session Id
82          *
83          * @return A bundle containing the session Id
84          */
85         @JvmStatic
86         fun createArgs(sessionId: Long): Bundle {
87             val bundle = Bundle()
88             bundle.putLong(EXTRA_SESSION_ID, sessionId)
89             return bundle
90         }
91     }
92 
93     override fun onCreate(savedInstanceState: Bundle?) {
94         super.onCreate(savedInstanceState)
95         val preferenceFragment: PF = requirePreferenceFragment()
96         isFirstLoad = true
97 
98         collator = Collator.getInstance(
99             context!!.getResources().getConfiguration().getLocales().get(0))
100         sessionId = arguments!!.getLong(EXTRA_SESSION_ID, INVALID_SESSION_ID)
101         val factory = UnusedAppsViewModelFactory(activity!!.application, sessionId)
102         viewModel = ViewModelProvider(this, factory).get(UnusedAppsViewModel::class.java)
103         viewModel.unusedPackageCategoriesLiveData.observe(this, Observer {
104             it?.let { pkgs ->
105                 updatePackages(pkgs)
106                 preferenceFragment.setLoadingState(loading = false, animate = true)
107             }
108         })
109 
110         activity?.getActionBar()?.setDisplayHomeAsUpEnabled(true)
111 
112         if (!viewModel.unusedPackageCategoriesLiveData.isInitialized) {
113             GlobalScope.launch(IPC) {
114                 delay(SHOW_LOAD_DELAY_MS)
115                 if (!viewModel.unusedPackageCategoriesLiveData.isInitialized) {
116                     GlobalScope.launch(Main) {
117                         preferenceFragment.setLoadingState(loading = true, animate = true)
118                     }
119                 } else {
120                     GlobalScope.launch(Main) {
121                         updatePackages(viewModel.unusedPackageCategoriesLiveData.value!!)
122                     }
123                 }
124             }
125         } else {
126             updatePackages(viewModel.unusedPackageCategoriesLiveData.value!!)
127         }
128     }
129 
130     override fun onStart() {
131         super.onStart()
132         val ab = activity?.actionBar
133         if (ab != null) {
134             ab!!.setElevation(ELEVATION_HIGH)
135         }
136     }
137 
138     override fun onActivityCreated(savedInstanceState: Bundle?) {
139         super.onActivityCreated(savedInstanceState)
140         val preferenceFragment: PF = requirePreferenceFragment()
141         if (isHibernationEnabled()) {
142             preferenceFragment.setTitle(getString(R.string.unused_apps_page_title))
143         } else {
144             preferenceFragment.setTitle(getString(R.string.permission_removed_page_title))
145         }
146     }
147 
148     private fun requirePreferenceFragment(): PF {
149         return requireParentFragment() as PF
150     }
151 
152     /**
153      * Create [PreferenceScreen] in the parent fragment.
154      */
155     private fun createPreferenceScreen() {
156         val preferenceFragment: PF = requirePreferenceFragment()
157         val preferenceScreen = preferenceFragment.preferenceManager.inflateFromResource(
158             context!!,
159             R.xml.unused_app_categories,
160             /* rootPreferences= */ null)
161         preferenceFragment.preferenceScreen = preferenceScreen
162 
163         val infoMsgCategory = preferenceScreen.findPreference<PreferenceCategory>(INFO_MSG_CATEGORY)
164         val footerPreference = preferenceFragment.createFooterPreference()
165         footerPreference.key = INFO_MSG_KEY
166         infoMsgCategory?.addPreference(footerPreference)
167     }
168 
169     private fun updatePackages(categorizedPackages: Map<Months, List<UnusedPackageInfo>>) {
170         val preferenceFragment: PF = requirePreferenceFragment()
171         if (preferenceFragment.preferenceScreen == null) {
172             createPreferenceScreen()
173         }
174         val preferenceScreen: PreferenceScreen = preferenceFragment.preferenceScreen
175 
176         val removedPrefs = mutableMapOf<String, UnusedAppPref>()
177         for (month in Months.allMonths()) {
178             val category = preferenceScreen.findPreference<PreferenceCategory>(month.value)!!
179             for (i in 0 until category.preferenceCount) {
180                 val pref = category.getPreference(i) as UnusedAppPref
181                 val contains = categorizedPackages[Months.THREE]?.any { (pkgName, user, _) ->
182                     val key = createKey(pkgName, user)
183                     pref.key == key
184                 }
185                 if (contains != true) {
186                     removedPrefs[pref.key] = pref
187                 }
188             }
189 
190             for ((_, pref) in removedPrefs) {
191                 category.removePreference(pref)
192             }
193         }
194 
195         var allCategoriesEmpty = true
196         for ((month, packages) in categorizedPackages) {
197             val category = preferenceScreen.findPreference<PreferenceCategory>(month.value)!!
198             category.title = if (month == Months.THREE) {
199                 getString(R.string.last_opened_category_title, "3")
200             } else {
201                 getString(R.string.last_opened_category_title, "6")
202             }
203             category.isVisible = packages.isNotEmpty()
204             if (packages.isNotEmpty()) {
205                 allCategoriesEmpty = false
206             }
207 
208             for ((pkgName, user, isSystemApp, permSet) in packages) {
209                 val revokedPerms = permSet.toList()
210                 val key = createKey(pkgName, user)
211 
212                 var pref = category.findPreference<UnusedAppPref>(key)
213                 if (pref == null) {
214                     pref = removedPrefs[key] ?: preferenceFragment.createUnusedAppPref(
215                         activity!!.application, pkgName, user)
216                     pref.key = key
217                     pref.title = KotlinUtils.getPackageLabel(activity!!.application, pkgName, user)
218                 }
219 
220                 pref.setRemoveClickRunnable {
221                     viewModel.requestUninstallApp(this, pkgName, user)
222                 }
223                 pref.setRemoveComponentEnabled(!isSystemApp)
224 
225                 pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
226                     viewModel.navigateToAppInfo(pkgName, user, sessionId)
227                     true
228                 }
229 
230                 val mostImportant = getMostImportantGroup(revokedPerms)
231                 val importantLabel = KotlinUtils.getPermGroupLabel(context!!, mostImportant)
232                 pref.summary = when {
233                     revokedPerms.isEmpty() -> null
234                     revokedPerms.size == 1 -> getString(R.string.auto_revoked_app_summary_one,
235                         importantLabel)
236                     revokedPerms.size == 2 -> {
237                         val otherLabel = if (revokedPerms[0] == mostImportant) {
238                             KotlinUtils.getPermGroupLabel(context!!, revokedPerms[1])
239                         } else {
240                             KotlinUtils.getPermGroupLabel(context!!, revokedPerms[0])
241                         }
242                         getString(R.string.auto_revoked_app_summary_two, importantLabel, otherLabel)
243                     }
244                     else -> getString(R.string.auto_revoked_app_summary_many, importantLabel,
245                         "${revokedPerms.size - 1}")
246                 }
247                 category.addPreference(pref)
248                 KotlinUtils.sortPreferenceGroup(category, this::comparePreference, false)
249             }
250         }
251 
252         preferenceFragment.setEmptyState(allCategoriesEmpty)
253 
254         if (isFirstLoad) {
255             if (categorizedPackages[Months.SIX]!!.isNotEmpty() ||
256                 categorizedPackages[Months.THREE]!!.isNotEmpty()) {
257                 isFirstLoad = false
258             }
259             Log.i(LOG_TAG, "sessionId: $sessionId Showed Auto Revoke Page")
260             for (month in Months.values()) {
261                 Log.i(LOG_TAG, "sessionId: $sessionId $month unused: " +
262                     "${categorizedPackages[month]}")
263                 for (revokedPackageInfo in categorizedPackages[month]!!) {
264                     for (groupName in revokedPackageInfo.revokedGroups) {
265                         val isNewlyRevoked = month == Months.THREE
266                         viewModel.logAppView(revokedPackageInfo.packageName,
267                             revokedPackageInfo.user, groupName, isNewlyRevoked)
268                     }
269                 }
270             }
271         }
272     }
273 
274     private fun comparePreference(lhs: Preference, rhs: Preference): Int {
275         var result = collator.compare(lhs.title.toString(),
276             rhs.title.toString())
277         if (result == 0) {
278             result = lhs.key.compareTo(rhs.key)
279         }
280         return result
281     }
282 
283     private fun createKey(packageName: String, user: UserHandle): String {
284         return "$packageName:${user.identifier}"
285     }
286 
287     private fun getMostImportantGroup(groupNames: List<String>): String {
288         return when {
289             groupNames.contains(permission_group.LOCATION) -> permission_group.LOCATION
290             groupNames.contains(permission_group.MICROPHONE) -> permission_group.MICROPHONE
291             groupNames.contains(permission_group.CAMERA) -> permission_group.CAMERA
292             groupNames.contains(permission_group.CONTACTS) -> permission_group.CONTACTS
293             groupNames.contains(permission_group.STORAGE) -> permission_group.STORAGE
294             groupNames.contains(permission_group.CALENDAR) -> permission_group.CALENDAR
295             groupNames.isNotEmpty() -> groupNames[0]
296             else -> ""
297         }
298     }
299 
300     private fun createDisableDialog(packageName: String, user: UserHandle) {
301         val dialog = DisableDialog()
302 
303         val args = Bundle()
304         args.putString(Intent.EXTRA_PACKAGE_NAME, packageName)
305         args.putParcelable(Intent.EXTRA_USER, user)
306         dialog.arguments = args
307 
308         dialog.isCancelable = true
309 
310         dialog.show(childFragmentManager.beginTransaction(), DisableDialog::class.java.name)
311     }
312 
313     class DisableDialog : DialogFragment() {
314         override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
315             val fragment = parentFragment as UnusedAppsFragment<*, *>
316             val packageName = arguments!!.getString(Intent.EXTRA_PACKAGE_NAME)!!
317             val user = arguments!!.getParcelable<UserHandle>(Intent.EXTRA_USER)!!
318             val b = AlertDialog.Builder(context!!)
319                 .setMessage(R.string.app_disable_dlg_text)
320                 .setPositiveButton(R.string.app_disable_dlg_positive) { _, _ ->
321                     fragment.viewModel.disableApp(packageName, user)
322                 }
323                 .setNegativeButton(R.string.cancel, null)
324             val d: Dialog = b.create()
325             d.setCanceledOnTouchOutside(true)
326             return d
327         }
328     }
329 
330     /**
331      * Interface that the parent fragment must implement.
332      */
333     interface Parent<UnusedAppPref> where UnusedAppPref : Preference,
334                                            UnusedAppPref : RemovablePref {
335 
336         /**
337          * Set the title of the current settings page.
338          *
339          * @param title the title of the current settings page
340          */
341         fun setTitle(title: CharSequence)
342 
343         /**
344          * Creates the footer preference that explains why permissions have been re-used and how an
345          * app can re-request them.
346          */
347         fun createFooterPreference(): Preference
348 
349         /**
350          * Sets the loading state of the view.
351          *
352          * @param loading whether the view is loading
353          * @param animate whether the load state should change with a fade animation
354          */
355         fun setLoadingState(loading: Boolean, animate: Boolean)
356 
357         /**
358          * Creates a preference which represents an app that is unused. Has the app icon and label,
359          * as well as a button to uninstall/disable the app, and a button to open the app.
360          *
361          * @param app The current application
362          * @param packageName The name of the package whose icon this preference will retrieve
363          * @param user The user whose package icon will be retrieved
364          */
365         fun createUnusedAppPref(
366             app: Application,
367             packageName: String,
368             user: UserHandle
369         ): UnusedAppPref
370 
371         /**
372          * Updates the state based on whether the content is empty.
373          *
374          * @param empty whether the content is empty
375          */
376         fun setEmptyState(empty: Boolean)
377     }
378 }
379