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 }