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 }