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