summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt341
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt234
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt798
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt404
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt51
7 files changed, 1845 insertions, 5 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
new file mode 100644
index 000000000000..805a102b1a1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.Manifest
+import android.app.ActivityManager
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.util.Log
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private val defaultDialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ starter: (String, Int, CharSequence?, Intent?) -> Unit
+ ): PrivacyDialogV2 {
+ return PrivacyDialogV2(context, list, starter)
+ }
+}
+
+/**
+ * Controller for [PrivacyDialogV2].
+ *
+ * This controller shows and dismissed the dialog, as well as determining the information to show in
+ * it.
+ */
+@SysUISingleton
+class PrivacyDialogControllerV2(
+ private val permissionManager: PermissionManager,
+ private val packageManager: PackageManager,
+ private val privacyItemController: PrivacyItemController,
+ private val userTracker: UserTracker,
+ private val activityStarter: ActivityStarter,
+ private val backgroundExecutor: Executor,
+ private val uiExecutor: Executor,
+ private val privacyLogger: PrivacyLogger,
+ private val keyguardStateController: KeyguardStateController,
+ private val appOpsController: AppOpsController,
+ private val uiEventLogger: UiEventLogger,
+ @VisibleForTesting private val dialogProvider: DialogProvider
+) {
+
+ @Inject
+ constructor(
+ permissionManager: PermissionManager,
+ packageManager: PackageManager,
+ privacyItemController: PrivacyItemController,
+ userTracker: UserTracker,
+ activityStarter: ActivityStarter,
+ @Background backgroundExecutor: Executor,
+ @Main uiExecutor: Executor,
+ privacyLogger: PrivacyLogger,
+ keyguardStateController: KeyguardStateController,
+ appOpsController: AppOpsController,
+ uiEventLogger: UiEventLogger
+ ) : this(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ defaultDialogProvider
+ )
+
+ companion object {
+ private const val TAG = "PrivacyDialogController"
+ }
+
+ private var dialog: Dialog? = null
+
+ private val onDialogDismissed = object : PrivacyDialogV2.OnDialogDismissed {
+ override fun onDialogDismissed() {
+ privacyLogger.logPrivacyDialogDismissed()
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ dialog = null
+ }
+ }
+
+ @MainThread
+ private fun startActivity(
+ packageName: String,
+ userId: Int,
+ attributionTag: CharSequence?,
+ navigationIntent: Intent?
+ ) {
+ val intent = if (navigationIntent == null) {
+ getDefaultManageAppPermissionsIntent(packageName, userId)
+ } else {
+ navigationIntent
+ }
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ userId, packageName)
+ privacyLogger.logStartSettingsActivityFromDialog(packageName, userId)
+ if (!keyguardStateController.isUnlocked) {
+ // If we are locked, hide the dialog so the user can unlock
+ dialog?.hide()
+ }
+ // startActivity calls internally startActivityDismissingKeyguard
+ activityStarter.startActivity(intent, true) {
+ if (ActivityManager.isStartResultSuccessful(it)) {
+ dismissDialog()
+ } else {
+ dialog?.show()
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun getManagePermissionIntent(
+ packageName: String,
+ userId: Int,
+ permGroupName: CharSequence,
+ attributionTag: CharSequence?,
+ isAttributionSupported: Boolean
+ ): Intent
+ {
+ lateinit var intent: Intent
+ if (attributionTag != null && isAttributionSupported) {
+ intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ intent.setPackage(packageName)
+ intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName.toString())
+ intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString()))
+ intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true)
+ val resolveInfo = packageManager.resolveActivity(
+ intent, PackageManager.ResolveInfoFlags.of(0))
+ if (resolveInfo != null && resolveInfo.activityInfo != null &&
+ resolveInfo.activityInfo.permission ==
+ android.Manifest.permission.START_VIEW_PERMISSION_USAGE) {
+ intent.component = ComponentName(packageName, resolveInfo.activityInfo.name)
+ return intent
+ }
+ }
+ return getDefaultManageAppPermissionsIntent(packageName, userId)
+ }
+
+ fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
+ val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS)
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
+ intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
+ return intent
+ }
+
+ @WorkerThread
+ private fun permGroupUsage(): List<PermissionGroupUsage> {
+ return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
+ }
+
+ /**
+ * Show the [PrivacyDialogV2]
+ *
+ * This retrieves the permission usage from [PermissionManager] and creates a new
+ * [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show.
+ *
+ * This list will be filtered by [filterAndSelect]. Only types available by
+ * [PrivacyItemController] will be shown.
+ *
+ * @param context A context to use to create the dialog.
+ * @see filterAndSelect
+ */
+ fun showDialog(context: Context) {
+ dismissDialog()
+ backgroundExecutor.execute {
+ val usage = permGroupUsage()
+ val userInfos = userTracker.userProfiles
+ privacyLogger.logUnfilteredPermGroupUsage(usage)
+ val items = usage.mapNotNull {
+ val type = filterType(permGroupToPrivacyType(it.permissionGroupName))
+ val userInfo = userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
+ if (userInfo != null || it.isPhoneCall) {
+ type?.let { t ->
+ // Only try to get the app name if we actually need it
+ val appName = if (it.isPhoneCall) {
+ ""
+ } else {
+ getLabelForPackage(it.packageName, it.uid)
+ }
+ val userId = UserHandle.getUserId(it.uid)
+ PrivacyDialogV2.PrivacyElement(
+ t,
+ it.packageName,
+ userId,
+ appName,
+ it.attributionTag,
+ it.attributionLabel,
+ it.proxyLabel,
+ it.lastAccessTimeMillis,
+ it.isActive,
+ // If there's no user info, we're in a phoneCall in secondary user
+ userInfo?.isManagedProfile ?: false,
+ it.isPhoneCall,
+ it.permissionGroupName,
+ getManagePermissionIntent(
+ it.packageName,
+ userId,
+ it.permissionGroupName,
+ it.attributionTag,
+ // attributionLabel is set only when subattribution policies
+ // are supported and satisfied
+ it.attributionLabel != null
+ )
+ )
+ }
+ } else {
+ // No matching user or phone call
+ null
+ }
+ }
+ uiExecutor.execute {
+ val elements = filterAndSelect(items)
+ if (elements.isNotEmpty()) {
+ val d = dialogProvider.makeDialog(context, elements, this::startActivity)
+ d.setShowForAllUsers(true)
+ d.addOnDismissListener(onDialogDismissed)
+ d.show()
+ privacyLogger.logShowDialogV2Contents(elements)
+ dialog = d
+ } else {
+ Log.w(TAG, "Trying to show empty dialog")
+ }
+ }
+ }
+ }
+
+ /**
+ * Dismisses the dialog
+ */
+ fun dismissDialog() {
+ dialog?.dismiss()
+ }
+
+ @WorkerThread
+ private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
+ return try {
+ packageManager
+ .getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
+ .loadLabel(packageManager)
+ } catch (_: PackageManager.NameNotFoundException) {
+ Log.w(TAG, "Label not found for: $packageName")
+ packageName
+ }
+ }
+
+ private fun permGroupToPrivacyType(group: String): PrivacyType? {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
+ Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
+ Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
+ else -> null
+ }
+ }
+
+ private fun filterType(type: PrivacyType?): PrivacyType? {
+ return type?.let {
+ if ((it == PrivacyType.TYPE_CAMERA || it == PrivacyType.TYPE_MICROPHONE) &&
+ privacyItemController.micCameraAvailable) {
+ it
+ } else if (it == PrivacyType.TYPE_LOCATION && privacyItemController.locationAvailable) {
+ it
+ } else {
+ null
+ }
+ }
+ }
+
+ /**
+ * Filters the list of elements to show.
+ *
+ * For each privacy type, it'll return all active elements. If there are no active elements,
+ * it'll return the most recent access
+ */
+ private fun filterAndSelect(
+ list: List<PrivacyDialogV2.PrivacyElement>
+ ): List<PrivacyDialogV2.PrivacyElement> {
+ return list.groupBy { it.type }.toSortedMap().flatMap { (_, elements) ->
+ val actives = elements.filter { it.active }
+ if (actives.isNotEmpty()) {
+ actives.sortedByDescending { it.lastActiveTimestamp }
+ } else {
+ elements.maxByOrNull { it.lastActiveTimestamp }?.let {
+ listOf(it)
+ } ?: emptyList()
+ }
+ }
+ }
+
+ /**
+ * Interface to create a [PrivacyDialogV2].
+ *
+ * Can be used to inject a mock creator.
+ */
+ interface DialogProvider {
+ /**
+ * Create a [PrivacyDialogV2].
+ */
+ fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ starter: (String, Int, CharSequence?, Intent?) -> Unit
+ ): PrivacyDialogV2
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
new file mode 100644
index 000000000000..a00775ad0e4b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.LayerDrawable
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Dialog to show ongoing and recent app ops usage.
+ *
+ * @see PrivacyDialogV2Controller
+ * @param context A context to create the dialog
+ * @param list list of elements to show in the dialog. The elements will show in the same order they
+ * appear in the list
+ * @param activityStarter a callback to start an activity for a given package name, user id, attributionTag and intent
+ */
+class PrivacyDialogV2(
+ context: Context,
+ private val list: List<PrivacyElement>,
+ activityStarter: (String, Int, CharSequence?, Intent?) -> Unit
+) : SystemUIDialog(context, R.style.PrivacyDialog) {
+
+ private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
+ private val dismissed = AtomicBoolean(false)
+
+ private val iconColorSolid = Utils.getColorAttrDefaultColor(
+ this.context, com.android.internal.R.attr.colorPrimary
+ )
+ private val enterpriseText = " ${context.getString(R.string.ongoing_privacy_dialog_enterprise)}"
+ private val phonecall = context.getString(R.string.ongoing_privacy_dialog_phonecall)
+
+ private lateinit var rootView: ViewGroup
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window?.apply {
+ attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
+ attributes.receiveInsetsIgnoringZOrder = true
+ setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
+ }
+ setTitle(R.string.ongoing_privacy_dialog_a11y_title)
+ setContentView(R.layout.privacy_dialog)
+ rootView = requireViewById<ViewGroup>(R.id.root)
+
+ list.forEach {
+ rootView.addView(createView(it))
+ }
+ }
+
+ /**
+ * Add a listener that will be called when the dialog is dismissed.
+ *
+ * If the dialog has already been dismissed, the listener will be called immediately, in the
+ * same thread.
+ */
+ fun addOnDismissListener(listener: OnDialogDismissed) {
+ if (dismissed.get()) {
+ listener.onDialogDismissed()
+ } else {
+ dismissListeners.add(WeakReference(listener))
+ }
+ }
+
+ override fun stop() {
+ dismissed.set(true)
+ val iterator = dismissListeners.iterator()
+ while (iterator.hasNext()) {
+ val el = iterator.next()
+ iterator.remove()
+ el.get()?.onDialogDismissed()
+ }
+ }
+
+ private fun createView(element: PrivacyElement): View {
+ val newView = LayoutInflater.from(context).inflate(
+ R.layout.privacy_dialog_item, rootView, false
+ ) as ViewGroup
+ val d = getDrawableForType(element.type)
+ d.findDrawableByLayerId(R.id.icon).setTint(iconColorSolid)
+ newView.requireViewById<ImageView>(R.id.icon).apply {
+ setImageDrawable(d)
+ contentDescription = element.type.getName(context)
+ }
+ val stringId = getStringIdForState(element.active)
+ val app = if (element.phoneCall) phonecall else element.applicationName
+ val appName = if (element.enterprise) {
+ TextUtils.concat(app, enterpriseText)
+ } else {
+ app
+ }
+ val firstLine = context.getString(stringId, appName)
+ val finalText = getFinalText(firstLine, element.attributionLabel, element.proxyLabel)
+ newView.requireViewById<TextView>(R.id.text).text = finalText
+ if (element.phoneCall) {
+ newView.requireViewById<View>(R.id.chevron).visibility = View.GONE
+ }
+ newView.apply {
+ setTag(element)
+ if (!element.phoneCall) {
+ setOnClickListener(clickListener)
+ }
+ }
+ return newView
+ }
+
+ private fun getFinalText(
+ firstLine: CharSequence,
+ attributionLabel: CharSequence?,
+ proxyLabel: CharSequence?
+ ): CharSequence {
+ var dialogText: CharSequence? = null
+ if (attributionLabel != null && proxyLabel != null) {
+ dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_proxy_label,
+ attributionLabel, proxyLabel)
+ } else if (attributionLabel != null) {
+ dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_label,
+ attributionLabel)
+ } else if (proxyLabel != null) {
+ dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_text,
+ proxyLabel)
+ }
+ return if (dialogText != null) TextUtils.concat(firstLine, " ", dialogText) else firstLine
+ }
+
+ private fun getStringIdForState(active: Boolean): Int {
+ return if (active) {
+ R.string.ongoing_privacy_dialog_using_op
+ } else {
+ R.string.ongoing_privacy_dialog_recent_op
+ }
+ }
+
+ private fun getDrawableForType(type: PrivacyType): LayerDrawable {
+ return context.getDrawable(when (type) {
+ PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
+ PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
+ PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
+ PrivacyType.TYPE_MEDIA_PROJECTION -> R.drawable.privacy_item_circle_media_projection
+ }) as LayerDrawable
+ }
+
+ private val clickListener = View.OnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ activityStarter(element.packageName, element.userId,
+ element.attributionTag, element.navigationIntent)
+ }
+ }
+
+ /** */
+ data class PrivacyElement(
+ val type: PrivacyType,
+ val packageName: String,
+ val userId: Int,
+ val applicationName: CharSequence,
+ val attributionTag: CharSequence?,
+ val attributionLabel: CharSequence?,
+ val proxyLabel: CharSequence?,
+ val lastActiveTimestamp: Long,
+ val active: Boolean,
+ val enterprise: Boolean,
+ val phoneCall: Boolean,
+ val permGroupName: CharSequence,
+ val navigationIntent: Intent?
+ ) {
+ private val builder = StringBuilder("PrivacyElement(")
+
+ init {
+ builder.append("type=${type.logName}")
+ builder.append(", packageName=$packageName")
+ builder.append(", userId=$userId")
+ builder.append(", appName=$applicationName")
+ if (attributionTag != null) {
+ builder.append(", attributionTag=$attributionTag")
+ }
+ if (attributionLabel != null) {
+ builder.append(", attributionLabel=$attributionLabel")
+ }
+ if (proxyLabel != null) {
+ builder.append(", proxyLabel=$proxyLabel")
+ }
+ builder.append(", lastActive=$lastActiveTimestamp")
+ if (active) {
+ builder.append(", active")
+ }
+ if (enterprise) {
+ builder.append(", enterprise")
+ }
+ if (phoneCall) {
+ builder.append(", phoneCall")
+ }
+ builder.append(", permGroupName=$permGroupName)")
+ if (navigationIntent != null) {
+ builder.append(", navigationIntent=$navigationIntent")
+ }
+ }
+
+ override fun toString(): String = builder.toString()
+ }
+
+ /** */
+ interface OnDialogDismissed {
+ fun onDialogDismissed()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index f934346d9775..26c4df8d4536 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -23,6 +23,7 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogMessage
import com.android.systemui.privacy.PrivacyDialog
+import com.android.systemui.privacy.PrivacyDialogV2
import com.android.systemui.privacy.PrivacyItem
import java.util.Locale
import javax.inject.Inject
@@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) {
+ log(LogLevel.INFO, {
+ str1 = contents.toString()
+ }, {
+ "Privacy dialog shown. Contents: $str1"
+ })
+ }
+
fun logEmptyDialog() {
log(LogLevel.WARNING, {}, {
"Trying to show an empty dialog"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
index 995c6a476f0d..d41ae0bb026b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
@@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyChipEvent
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
@@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor(
private val uiEventLogger: UiEventLogger,
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
private val privacyDialogController: PrivacyDialogController,
+ private val privacyDialogControllerV2: PrivacyDialogControllerV2,
private val privacyLogger: PrivacyLogger,
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
private val permissionManager: PermissionManager,
@@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor(
private val appOpsController: AppOpsController,
private val broadcastDispatcher: BroadcastDispatcher,
private val safetyCenterManager: SafetyCenterManager,
- private val deviceProvisionedController: DeviceProvisionedController
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val featureFlags: FeatureFlags
) {
var chipVisibilityListener: ChipVisibilityListener? = null
@@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor(
// If the privacy chip is visible, it means there were some indicators
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) {
- showSafetyCenter()
+ if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
+ privacyDialogControllerV2.showDialog(privacyChip.context)
+ } else {
+ showSafetyCenter()
+ }
} else {
privacyDialogController.showDialog(privacyChip.context)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
new file mode 100644
index 000000000000..22ec26e35502
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
@@ -0,0 +1,798 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+import android.content.pm.UserInfo
+import android.os.Process.SYSTEM_UID
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PrivacyDialogControllerV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val USER_ID = 0
+ private const val ENT_USER_ID = 10
+
+ private const val TEST_PACKAGE_NAME = "test package name"
+ private const val TEST_ATTRIBUTION_TAG = "test attribution tag"
+ private const val TEST_PROXY_LABEL = "test proxy label"
+
+ private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA
+ private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE
+ private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION
+ }
+
+ @Mock
+ private lateinit var dialog: PrivacyDialogV2
+ @Mock
+ private lateinit var permissionManager: PermissionManager
+ @Mock
+ private lateinit var packageManager: PackageManager
+ @Mock
+ private lateinit var privacyItemController: PrivacyItemController
+ @Mock
+ private lateinit var userTracker: UserTracker
+ @Mock
+ private lateinit var activityStarter: ActivityStarter
+ @Mock
+ private lateinit var privacyLogger: PrivacyLogger
+ @Mock
+ private lateinit var keyguardStateController: KeyguardStateController
+ @Mock
+ private lateinit var appOpsController: AppOpsController
+ @Captor
+ private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed>
+ @Captor
+ private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
+ @Captor
+ private lateinit var intentCaptor: ArgumentCaptor<Intent>
+ @Mock
+ private lateinit var uiEventLogger: UiEventLogger
+
+ private val backgroundExecutor = FakeExecutor(FakeSystemClock())
+ private val uiExecutor = FakeExecutor(FakeSystemClock())
+ private lateinit var controller: PrivacyDialogControllerV2
+ private var nextUid: Int = 0
+
+ private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
+ var list: List<PrivacyDialogV2.PrivacyElement>? = null
+ var starter: ((String, Int, CharSequence?, Intent?) -> Unit)? = null
+
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ starter: (String, Int, CharSequence?, Intent?) -> Unit
+ ): PrivacyDialogV2 {
+ this.list = list
+ this.starter = starter
+ return dialog
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ nextUid = 0
+ setUpDefaultMockResponses()
+
+ controller = PrivacyDialogControllerV2(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogProvider
+ )
+ }
+
+ @After
+ fun tearDown() {
+ FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
+ dialogProvider.list = null
+ dialogProvider.starter = null
+ }
+
+ @Test
+ fun testMicMutedParameter() {
+ `when`(appOpsController.isMicMuted).thenReturn(true)
+ controller.showDialog(context)
+ backgroundExecutor.runAllReady()
+
+ verify(permissionManager).getIndicatorAppOpUsageData(true)
+ }
+
+ @Test
+ fun testPermissionManagerOnlyCalledInBackgroundThread() {
+ controller.showDialog(context)
+ verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean())
+ backgroundExecutor.runAllReady()
+ verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean())
+ }
+
+ @Test
+ fun testPackageManagerOnlyCalledInBackgroundThread() {
+ val usage = createMockPermGroupUsage()
+ `when`(usage.isPhoneCall).thenReturn(false)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ backgroundExecutor.runAllReady()
+ verify(packageManager, atLeastOnce())
+ .getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ }
+
+ @Test
+ fun testShowDialogShowsDialog() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testDontShowEmptyDialog() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testHideDialogDismissesDialogIfShown() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ controller.dismissDialog()
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopIfNotShown() {
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopAfterDismissed() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testShowForAllUsers() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+
+ exhaustExecutors()
+ verify(dialog).setShowForAllUsers(true)
+ }
+
+ @Test
+ fun testSingleElementInList() {
+ val usage = createMockPermGroupUsage(
+ packageName = TEST_PACKAGE_NAME,
+ uid = generateUidForUser(USER_ID),
+ permissionGroupName = PERM_CAMERA,
+ lastAccessTimeMillis = 5L,
+ isActive = true,
+ isPhoneCall = false,
+ attributionTag = null,
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
+ assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).userId).isEqualTo(USER_ID)
+ assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).attributionTag).isNull()
+ assertThat(list.get(0).attributionLabel).isNull()
+ assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
+ assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
+ assertThat(list.get(0).active).isTrue()
+ assertThat(list.get(0).phoneCall).isFalse()
+ assertThat(list.get(0).enterprise).isFalse()
+ assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ private fun isIntentEqual(actual: Intent, expected: Intent): Boolean {
+ return actual.action == expected.action &&
+ actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) ==
+ expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) &&
+ actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle ==
+ expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle
+ }
+
+ @Test
+ fun testTwoElementsDifferentType_sorted() {
+ val usage_camera = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_camera",
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_microphone",
+ permissionGroupName = PERM_MICROPHONE
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_microphone, usage_camera)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list).hasSize(2)
+ assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
+ }
+ }
+
+ @Test
+ fun testTwoElementsSameType_oneActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true
+ )
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_active)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.active).isTrue()
+ }
+
+ @Test
+ fun testTwoElementsSameType_twoActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_active_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active_recent",
+ isActive = true,
+ lastAccessTimeMillis = 1L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_active, usage_active_moreRecent)
+ )
+ controller.showDialog(context)
+ exhaustExecutors()
+ assertThat(dialogProvider.list).hasSize(2)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
+ assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L)
+ }
+
+ @Test
+ fun testManyElementsSameType_bothRecent() {
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_moreRecent",
+ isActive = false,
+ lastAccessTimeMillis = 1L
+ )
+ val usage_mostRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_mostRecent",
+ isActive = false,
+ lastAccessTimeMillis = 2L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_mostRecent, usage_moreRecent)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
+ }
+
+ @Test
+ fun testMicAndCameraDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+
+ @Test
+ fun testLocationDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(2)
+ dialogProvider.list?.forEach {
+ assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+ }
+
+ @Test
+ fun testAllIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(3)
+ }
+
+ @Test
+ fun testNoIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testEnterpriseUser() {
+ val usage_enterprise = createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID)
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
+ .thenReturn(listOf(usage_enterprise))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list?.single()?.enterprise).isTrue()
+ }
+
+ @Test
+ fun testNotCurrentUser() {
+ val usage_other = createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID + 1)
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
+ .thenReturn(listOf(usage_other))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testStartActivityCorrectIntent() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
+ verify(activityStarter)
+ .startActivity(capture(intentCaptor), eq(true), any<ActivityStarter.Callback>())
+
+ assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MANAGE_APP_PERMISSIONS)
+ assertThat(intentCaptor.value.getStringExtra(Intent.EXTRA_PACKAGE_NAME))
+ .isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(intentCaptor.value.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle)
+ .isEqualTo(UserHandle.of(USER_ID))
+ }
+
+ @Test
+ fun testStartActivityCorrectIntent_enterpriseUser() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, ENT_USER_ID, null, null)
+ verify(activityStarter)
+ .startActivity(capture(intentCaptor), eq(true), any<ActivityStarter.Callback>())
+
+ assertThat(intentCaptor.value.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle)
+ .isEqualTo(UserHandle.of(ENT_USER_ID))
+ }
+
+ @Test
+ fun testStartActivitySuccess() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
+
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testStartActivityFailure() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
+
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testCallOnSecondaryUser() {
+ // Calls happen in
+ val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(ENT_USER_ID, "", 0)
+ ))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testStartActivityLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(TEST_PACKAGE_NAME, USER_ID, null, null)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testDismissedDialogLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+
+ controller.dismissDialog()
+
+ verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ }
+
+ @Test
+ fun testInvalidAttributionTag() {
+ val usage = createMockPermGroupUsage(
+ packageName = TEST_PACKAGE_NAME,
+ uid = generateUidForUser(USER_ID),
+ permissionGroupName = PERM_CAMERA,
+ lastAccessTimeMillis = 5L,
+ isActive = true,
+ isPhoneCall = false,
+ attributionTag = "INVALID_ATTRIBUTION_TAG",
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
+ assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).userId).isEqualTo(USER_ID)
+ assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).attributionTag).isEqualTo("INVALID_ATTRIBUTION_TAG")
+ assertThat(list.get(0).attributionLabel).isNull()
+ assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
+ assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
+ assertThat(list.get(0).active).isTrue()
+ assertThat(list.get(0).phoneCall).isFalse()
+ assertThat(list.get(0).enterprise).isFalse()
+ assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun testCorrectIntentSubAttribution() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG,
+ attributionLabel = "TEST_LABEL"
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ val navigationIntent = list.get(0).navigationIntent!!
+ assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
+ .isEqualTo(PERM_CAMERA)
+ assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS))
+ .isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
+ assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnMissingAttributionLabel() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnIncorrectPermission() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo(
+ permission = "INCORRECT_PERMISSION"
+ )
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ private fun exhaustExecutors() {
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+ }
+
+ private fun setUpDefaultMockResponses() {
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList())
+ `when`(appOpsController.isMicMuted).thenReturn(false)
+
+ `when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenAnswer { FakeApplicationInfo(it.getArgument(0)) }
+
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(USER_ID, "", 0),
+ UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
+ ))
+
+ `when`(keyguardStateController.isUnlocked).thenReturn(true)
+ }
+
+ private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return label
+ }
+ }
+
+ private fun generateUidForUser(user: Int): Int {
+ return user * UserHandle.PER_USER_RANGE + nextUid++
+ }
+
+ private fun createMockResolveInfo(
+ activityInfo: ActivityInfo? = null
+ ): ResolveInfo {
+ val resolveInfo = mock(ResolveInfo::class.java)
+ resolveInfo.activityInfo = activityInfo
+ return resolveInfo
+ }
+
+ private fun createMockActivityInfo(
+ permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE,
+ className: String = "TEST_CLASS_NAME"
+ ): ActivityInfo {
+ val activityInfo = mock(ActivityInfo::class.java)
+ activityInfo.permission = permission
+ activityInfo.name = className
+ return activityInfo
+ }
+
+ private fun createMockPermGroupUsage(
+ packageName: String = TEST_PACKAGE_NAME,
+ uid: Int = generateUidForUser(USER_ID),
+ permissionGroupName: String = PERM_CAMERA,
+ lastAccessTimeMillis: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null
+ ): PermissionGroupUsage {
+ val usage = mock(PermissionGroupUsage::class.java)
+ `when`(usage.packageName).thenReturn(packageName)
+ `when`(usage.uid).thenReturn(uid)
+ `when`(usage.permissionGroupName).thenReturn(permissionGroupName)
+ `when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis)
+ `when`(usage.isActive).thenReturn(isActive)
+ `when`(usage.isPhoneCall).thenReturn(isPhoneCall)
+ `when`(usage.attributionTag).thenReturn(attributionTag)
+ `when`(usage.attributionLabel).thenReturn(attributionLabel)
+ `when`(usage.proxyLabel).thenReturn(proxyLabel)
+ return usage
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
new file mode 100644
index 000000000000..62ce356d6cfa
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import android.content.Intent
+import android.text.TextUtils
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class PrivacyDialogV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val TEST_PACKAGE_NAME = "test_pkg"
+ private const val TEST_USER_ID = 0
+ private const val TEST_PERM_GROUP = "test_perm_group"
+ }
+
+ @Mock
+ private lateinit var starter: (String, Int, CharSequence?, Intent?) -> Unit
+ private lateinit var dialog: PrivacyDialogV2
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @After
+ fun teardown() {
+ if (this::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun testStarterCalledWithCorrectParams() {
+ val list = listOf(
+ PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ dialog.requireViewById<View>(R.id.privacy_item).callOnClick()
+ verify(starter).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, null, null)
+ }
+
+ @Test
+ fun testDismissListenerCalledOnDismiss() {
+ dialog = PrivacyDialogV2(context, emptyList(), starter)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+
+ verify(dismissListener, never()).onDialogDismissed()
+ dialog.dismiss()
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
+ dialog = PrivacyDialogV2(context, emptyList(), starter)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.show()
+ dialog.dismiss()
+
+ dialog.addOnDismissListener(dismissListener)
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testCorrectNumElements() {
+ val list = listOf(
+ PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_CAMERA,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ true,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ ),
+ PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<ViewGroup>(R.id.root).childCount).isEqualTo(2)
+ }
+
+ @Test
+ fun testUsingText() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_CAMERA,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ true,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text).isEqualTo(
+ context.getString(
+ R.string.ongoing_privacy_dialog_using_op,
+ element.applicationName,
+ element.type.getName(context)
+ )
+ )
+ }
+
+ @Test
+ fun testRecentText() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text).isEqualTo(
+ context.getString(
+ R.string.ongoing_privacy_dialog_recent_op,
+ element.applicationName,
+ element.type.getName(context)
+ )
+ )
+ }
+
+ @Test
+ fun testEnterprise() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ true,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
+ context.getString(R.string.ongoing_privacy_dialog_enterprise)
+ )
+ }
+
+ @Test
+ fun testPhoneCall() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ false,
+ true,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
+ context.getString(R.string.ongoing_privacy_dialog_phonecall)
+ )
+ }
+
+ @Test
+ fun testPhoneCallNotClickable() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ false,
+ true,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<View>(R.id.privacy_item).isClickable).isFalse()
+ assertThat(dialog.requireViewById<View>(R.id.chevron).visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testProxyLabel() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ "proxyLabel",
+ 0L,
+ false,
+ false,
+ true,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
+ context.getString(
+ R.string.ongoing_privacy_dialog_attribution_text,
+ element.proxyLabel
+ )
+ )
+ }
+
+ @Test
+ fun testSubattribution() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ "For subattribution",
+ null,
+ 0L,
+ true,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
+ context.getString(
+ R.string.ongoing_privacy_dialog_attribution_label,
+ element.attributionLabel
+ )
+ )
+ }
+
+ @Test
+ fun testSubattributionAndProxyLabel() {
+ val element = PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ "For subattribution",
+ "proxy label",
+ 0L,
+ true,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
+ context.getString(
+ R.string.ongoing_privacy_dialog_attribution_proxy_label,
+ element.attributionLabel, element.proxyLabel
+ )
+ )
+ }
+
+ @Test
+ fun testDialogHasTitle() {
+ // Dialog must have a non-empty title for a11y purposes.
+
+ val list = listOf(
+ PrivacyDialogV2.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ TEST_PACKAGE_NAME,
+ TEST_USER_ID,
+ "App",
+ null,
+ null,
+ null,
+ 0L,
+ false,
+ false,
+ false,
+ TEST_PERM_GROUP,
+ null
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, starter)
+ dialog.show()
+
+ assertThat(TextUtils.isEmpty(dialog.window?.attributes?.title)).isFalse()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
index 3620233fc9df..a6e471b57f75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
@@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -54,6 +57,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
@Mock
private lateinit var privacyDialogController: PrivacyDialogController
@Mock
+ private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2
+ @Mock
private lateinit var privacyLogger: PrivacyLogger
@Mock
private lateinit var iconContainer: StatusIconContainer
@@ -69,6 +74,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
private lateinit var safetyCenterManager: SafetyCenterManager
@Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock
+ private lateinit var featureFlags: FeatureFlags
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
@@ -94,6 +101,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
uiEventLogger,
privacyChip,
privacyDialogController,
+ privacyDialogControllerV2,
privacyLogger,
iconContainer,
permissionManager,
@@ -103,7 +111,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
appOpsController,
broadcastDispatcher,
safetyCenterManager,
- deviceProvisionedController
+ deviceProvisionedController,
+ featureFlags
)
backgroundExecutor.runAllReady()
@@ -154,17 +163,52 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
}
@Test
- fun testPrivacyChipClicked() {
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never()).showDialog(any(Context::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never()).showDialog(any(Context::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
+ verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
+ any(), any(), nullable(), anyInt(), nullable())
+ receiverCaptor.value.onReceive(
+ context,
+ Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
+ )
+ backgroundExecutor.runAllReady()
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController, never()).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never()).showDialog(any(Context::class.java))
}
@Test
- fun testSafetyCenterFlag() {
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
@@ -178,6 +222,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
+ verify(privacyDialogControllerV2).showDialog(any(Context::class.java))
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
}