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