diff --git a/packages/SystemUI/res/drawable/privacy_dialog_bg.xml b/packages/SystemUI/res/drawable/privacy_dialog_bg.xml
new file mode 100644
index 0000000000000..96136c4afab8b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_bg.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_item_circle_camera.xml b/packages/SystemUI/res/drawable/privacy_item_circle_camera.xml
new file mode 100644
index 0000000000000..cf6413642c632
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_item_circle_camera.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_item_circle_location.xml b/packages/SystemUI/res/drawable/privacy_item_circle_location.xml
new file mode 100644
index 0000000000000..0a6a4a31d0a54
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_item_circle_location.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_item_circle_microphone.xml b/packages/SystemUI/res/drawable/privacy_item_circle_microphone.xml
new file mode 100644
index 0000000000000..0a6a4a31d0a54
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_item_circle_microphone.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog.xml b/packages/SystemUI/res/layout/privacy_dialog.xml
new file mode 100644
index 0000000000000..5db247e25d503
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog.xml
@@ -0,0 +1,28 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_item.xml b/packages/SystemUI/res/layout/privacy_dialog_item.xml
new file mode 100644
index 0000000000000..882e9680407ee
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_item.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index c4cf440d4961a..3153d0d0123d4 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -96,4 +96,8 @@
#3C4043
+
+ #81C995
+ #FCAD70
+
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 3f6b8ef44589a..a7cf3e91dbbae 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -266,4 +266,8 @@
#F28B82
#E94235
+
+
+ #1E8E3E
+ #E8710A
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d104d17f37b7b..79cb236ff34a8 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1174,6 +1174,10 @@
16dp
+ @dimen/notification_corner_radius
+
+ @dimen/notification_shade_content_margin_horizontal
+
10sp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6e9c5dcde95be..5161dec1f2696 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2547,6 +2547,21 @@
\u0020and\u0020
+
+ %1$s is using the %2$s
+
+
+ %1$s used the %2$s recently
+
+
+ (enterprise)
+
+
+ Phonecall
+
+
+ (through %s)
+
camera
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 2c06c7b968319..04c4e977b2cff 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -54,6 +54,7 @@ import android.os.PowerManager;
import android.os.ServiceManager;
import android.os.UserManager;
import android.os.Vibrator;
+import android.permission.PermissionManager;
import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
import android.telecom.TelecomManager;
@@ -379,4 +380,10 @@ public class FrameworkServicesModule {
static WindowManager provideWindowManager(Context context) {
return context.getSystemService(WindowManager.class);
}
+
+ @Provides
+ @Singleton
+ static PermissionManager providePermissionManager(Context context) {
+ return context.getSystemService(PermissionManager.class);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt
new file mode 100644
index 0000000000000..66c535f869f03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.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.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+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 PrivacyDialogController
+ * @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 permission group name (as
+ * given by [PrivacyType.permGroupName])
+ */
+class PrivacyDialog(
+ context: Context,
+ private val list: List,
+ activityStarter: (String) -> Unit
+) : SystemUIDialog(context, R.style.ScreenRecord) {
+
+ private val dismissListeners = mutableListOf>()
+ 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()
+ setLayout(MATCH_PARENT, WRAP_CONTENT)
+ setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
+ setBackgroundDrawable(null)
+ }
+
+ setContentView(R.layout.privacy_dialog)
+ rootView = requireViewById(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 onStop() {
+ super.onStop()
+ 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(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, element.type.getName(context))
+ val finalText = element.attribution?.let {
+ TextUtils.concat(
+ firstLine,
+ "\n",
+ context.getString(R.string.ongoing_privacy_dialog_attribution_text, it)
+ )
+ } ?: firstLine
+ newView.requireViewById(R.id.text).text = finalText
+ newView.requireViewById(R.id.link).apply {
+ tag = element.type.permGroupName
+ setOnClickListener(clickListener)
+ }
+ return newView
+ }
+
+ 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
+ }) as LayerDrawable
+ }
+
+ private val clickListener = View.OnClickListener { v ->
+ if (v.id == R.id.link) {
+ v.tag?.let { activityStarter(it as String) }
+ }
+ }
+
+ /** */
+ data class PrivacyElement(
+ val type: PrivacyType,
+ val applicationName: CharSequence,
+ val attribution: CharSequence?,
+ val lastActiveTimestamp: Long,
+ val active: Boolean,
+ val enterprise: Boolean,
+ val phoneCall: Boolean
+ ) {
+ private val builder = StringBuilder("PrivacyElement(")
+
+ init {
+ builder.append("type=${type.logName}")
+ builder.append(", appName=$applicationName")
+ if (attribution != null) {
+ builder.append(", attribution=$attribution")
+ }
+ builder.append(", lastActive=$lastActiveTimestamp")
+ if (active) {
+ builder.append(", active")
+ }
+ if (enterprise) {
+ builder.append(", enterprise")
+ }
+ if (phoneCall) {
+ builder.append(", phoneCall")
+ }
+ builder.append(")")
+ }
+
+ 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/PrivacyDialogController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogController.kt
new file mode 100644
index 0000000000000..99e9bfc24e1d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogController.kt
@@ -0,0 +1,264 @@
+/*
+ * 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.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.permission.PermGroupUsage
+import android.permission.PermissionManager
+import android.util.Log
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+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 : PrivacyDialogController.DialogProvider {
+ override fun makeDialog(
+ context: Context,
+ list: List,
+ starter: (String) -> Unit
+ ): PrivacyDialog {
+ return PrivacyDialog(context, list, starter)
+ }
+}
+
+/**
+ * Controller for [PrivacyDialog].
+ *
+ * This controller shows and dismissed the dialog, as well as determining the information to show in
+ * it.
+ */
+@SysUISingleton
+class PrivacyDialogController(
+ 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,
+ @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
+ ) : this(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ defaultDialogProvider
+ )
+
+ companion object {
+ private const val TAG = "PrivacyDialogController"
+ }
+
+ private var dialog: Dialog? = null
+
+ private val onDialogDismissed = object : PrivacyDialog.OnDialogDismissed {
+ override fun onDialogDismissed() {
+ privacyLogger.logPrivacyDialogDismissed()
+ dialog = null
+ }
+ }
+
+ @MainThread
+ private fun startActivity(permGroupName: String) {
+ val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_APPS)
+ intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
+ privacyLogger.logStartSettingsActivityFromDialog(permGroupName)
+ 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 permGroupUsage(): List = permissionManager.indicatorAppOpUsageData
+
+ /**
+ * Show the [PrivacyDialog]
+ *
+ * This retrieves the permission usage from [PermissionManager] and creates a new
+ * [PrivacyDialog] with a list of [PrivacyDialog.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.permGroupName))
+ val userInfo = userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
+ userInfo?.let { ui ->
+ 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)
+ }
+ PrivacyDialog.PrivacyElement(
+ t,
+ appName,
+ it.attribution,
+ it.lastAccess,
+ it.isActive,
+ ui.isManagedProfile,
+ it.isPhoneCall
+ )
+ }
+ }
+ }
+ uiExecutor.execute {
+ val elements = filterAndSelect(items)
+ val d = dialogProvider.makeDialog(context, elements, this::startActivity)
+ d.setShowForAllUsers(true)
+ d.addOnDismissListener(onDialogDismissed)
+ d.show()
+ privacyLogger.logShowDialogContents(elements)
+ dialog = d
+ }
+ }
+ }
+
+ /**
+ * 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 (privacyItemController.allIndicatorsAvailable) {
+ it
+ } else 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.
+ *
+ * * Return at most one element per [PrivacyType], sorted by the natural order of the
+ * [PrivacyType].
+ * * If there are no active usages for a type, return the most recent
+ * * If there are multiple active usages for a type, return the most active recent.
+ */
+ private fun filterAndSelect(
+ list: List
+ ): List {
+ return list.groupBy { it.type }.toSortedMap().mapNotNull { entry ->
+ if (entry.value.isEmpty()) {
+ null
+ } else {
+ val actives = entry.value.filter { it.active }
+ val out = if (actives.isNotEmpty()) actives else entry.value
+ out.maxByOrNull { it.lastActiveTimestamp }
+ }
+ }
+ }
+
+ /**
+ * Interface to create a [PrivacyDialog].
+ *
+ * Can be used to inject a mock creator.
+ */
+ interface DialogProvider {
+ /**
+ * Create a [PrivacyDialog].
+ */
+ fun makeDialog(
+ context: Context,
+ list: List,
+ starter: (String) -> Unit
+ ): PrivacyDialog
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
index 7359e79b26f59..8a429e2e424d4 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
@@ -19,21 +19,29 @@ import com.android.systemui.R
typealias Privacy = PrivacyType
-enum class PrivacyType(val nameId: Int, val iconId: Int, val logName: String) {
- // This is uses the icons used by the corresponding permission groups in the AndroidManifest
+enum class PrivacyType(
+ val nameId: Int,
+ val iconId: Int,
+ val permGroupName: String,
+ val logName: String
+) {
+ // This uses the icons used by the corresponding permission groups in the AndroidManifest
TYPE_CAMERA(
R.string.privacy_type_camera,
com.android.internal.R.drawable.perm_group_camera,
+ android.Manifest.permission_group.CAMERA,
"camera"
),
TYPE_MICROPHONE(
R.string.privacy_type_microphone,
com.android.internal.R.drawable.perm_group_microphone,
+ android.Manifest.permission_group.MICROPHONE,
"microphone"
),
TYPE_LOCATION(
R.string.privacy_type_location,
com.android.internal.R.drawable.perm_group_location,
+ android.Manifest.permission_group.LOCATION,
"location"
);
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 f3b8d2e5fbc03..8059475dd832c 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -16,10 +16,12 @@
package com.android.systemui.privacy.logging
+import android.permission.PermGroupUsage
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel
import com.android.systemui.log.LogMessage
import com.android.systemui.log.dagger.PrivacyLog
+import com.android.systemui.privacy.PrivacyDialog
import javax.inject.Inject
private const val TAG = "PrivacyLog"
@@ -85,18 +87,48 @@ class PrivacyLogger @Inject constructor(
fun logStatusBarIconsVisible(
showCamera: Boolean,
- showMichrophone: Boolean,
+ showMicrophone: Boolean,
showLocation: Boolean
) {
log(LogLevel.INFO, {
bool1 = showCamera
- bool2 = showMichrophone
+ bool2 = showMicrophone
bool3 = showLocation
}, {
"Status bar icons visible: camera=$bool1, microphone=$bool2, location=$bool3"
})
}
+ fun logUnfilteredPermGroupUsage(contents: List) {
+ log(LogLevel.DEBUG, {
+ str1 = contents.toString()
+ }, {
+ "Perm group usage: $str1"
+ })
+ }
+
+ fun logShowDialogContents(contents: List) {
+ log(LogLevel.INFO, {
+ str1 = contents.toString()
+ }, {
+ "Privacy dialog shown. Contents: $str1"
+ })
+ }
+
+ fun logPrivacyDialogDismissed() {
+ log(LogLevel.INFO, {}, {
+ "Privacy dialog dismissed"
+ })
+ }
+
+ fun logStartSettingsActivityFromDialog(permGroupName: String) {
+ log(LogLevel.INFO, {
+ str1 = permGroupName
+ }, {
+ "Start settings activity from dialog for perm group: $str1"
+ })
+ }
+
private inline fun log(
logLevel: LogLevel,
initializer: LogMessage.() -> Unit,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index c5b76feb7cb39..e3c39aa77402d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -20,8 +20,6 @@ import android.app.AlarmManager.AlarmClockInfo;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.provider.AlarmClock;
import android.provider.Settings;
import android.service.notification.ZenModeConfig;
@@ -43,6 +41,7 @@ import com.android.systemui.demomode.DemoModeController;
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.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.privacy.logging.PrivacyLogger;
@@ -94,6 +93,7 @@ class QuickStatusBarHeaderController extends ViewController {
- mActivityStarter.postStartActivityDismissingKeyguard(
- new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0);
- mQSTileHost.collapsePanels();
- });
+ mPrivacyDialogController.showDialog(getContext());
} else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) {
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
Settings.ACTION_SOUND_SETTINGS), 0);
@@ -222,7 +217,8 @@ class QuickStatusBarHeaderController extends ViewController
+ @Captor
+ private lateinit var activityStartedCaptor: ArgumentCaptor
+ @Captor
+ private lateinit var intentCaptor: ArgumentCaptor
+
+ private val backgroundExecutor = FakeExecutor(FakeSystemClock())
+ private val uiExecutor = FakeExecutor(FakeSystemClock())
+ private lateinit var controller: PrivacyDialogController
+ private var nextUid: Int = 0
+
+ private val dialogProvider = object : PrivacyDialogController.DialogProvider {
+ var list: List? = null
+ var starter: ((String) -> Unit)? = null
+
+ override fun makeDialog(
+ context: Context,
+ list: List,
+ starter: (String) -> Unit
+ ): PrivacyDialog {
+ this.list = list
+ this.starter = starter
+ return dialog
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ nextUid = 0
+
+ setUpDefaultMockResponses()
+
+ controller = PrivacyDialogController(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ dialogProvider
+ )
+ }
+
+ @After
+ fun tearDown() {
+ FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
+ dialogProvider.list = null
+ dialogProvider.starter = null
+ }
+
+ @Test
+ fun testPermissionManagerOnlyCalledInBackgroundThread() {
+ controller.showDialog(context)
+ verify(permissionManager, never()).indicatorAppOpUsageData
+ backgroundExecutor.runAllReady()
+ verify(permissionManager).indicatorAppOpUsageData
+ }
+
+ @Test
+ fun testPackageManagerOnlyCalledInBackgroundThread() {
+ val usage = createMockPermGroupUsage()
+ `when`(usage.isPhoneCall).thenReturn(false)
+ `when`(permissionManager.indicatorAppOpUsageData).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() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testHideDialogDismissesDialogIfShown() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ controller.dismissDialog()
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopIfNotShown() {
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopAfterDismissed() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testShowForAllUsers() {
+ controller.showDialog(context)
+
+ exhaustExecutors()
+ verify(dialog).setShowForAllUsers(true)
+ }
+
+ @Test
+ fun testSingleElementInList() {
+ val usage = createMockPermGroupUsage(
+ packageName = TEST_PACKAGE_NAME,
+ uid = generateUidForUser(0),
+ permGroupName = PERM_CAMERA,
+ lastAccess = 5L,
+ isActive = true,
+ isPhoneCall = false,
+ attribution = TEST_ATTRIBUTION
+ )
+ `when`(permissionManager.indicatorAppOpUsageData).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ val expected = PrivacyDialog.PrivacyElement(
+ type = PrivacyType.TYPE_CAMERA,
+ applicationName = TEST_PACKAGE_NAME,
+ attribution = TEST_ATTRIBUTION,
+ lastActiveTimestamp = 5L,
+ active = true,
+ phoneCall = false,
+ enterprise = false
+ )
+ assertThat(dialogProvider.list).containsExactly(expected)
+ }
+
+ @Test
+ fun testTwoElementsDifferentType_sorted() {
+ val usage_camera = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_camera",
+ permGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_microphone",
+ permGroupName = PERM_MICROPHONE
+ )
+ `when`(permissionManager.indicatorAppOpUsageData).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.indicatorAppOpUsageData).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,
+ lastAccess = 0L
+ )
+ val usage_active_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active_recent",
+ isActive = true,
+ lastAccess = 1L
+ )
+ `when`(permissionManager.indicatorAppOpUsageData).thenReturn(
+ listOf(usage_active, usage_active_moreRecent)
+ )
+ controller.showDialog(context)
+ exhaustExecutors()
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
+ }
+
+ @Test
+ fun testManyElementsSameType_bothRecent() {
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false,
+ lastAccess = 0L
+ )
+ val usage_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_moreRecent",
+ isActive = false,
+ lastAccess = 1L
+ )
+ val usage_mostRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_mostRecent",
+ isActive = false,
+ lastAccess = 2L
+ )
+ `when`(permissionManager.indicatorAppOpUsageData).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(
+ permGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.indicatorAppOpUsageData).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(
+ permGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.indicatorAppOpUsageData).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(
+ permGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.indicatorAppOpUsageData).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+ `when`(privacyItemController.allIndicatorsAvailable).thenReturn(true)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(3)
+ }
+
+ @Test
+ fun testNoIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.indicatorAppOpUsageData).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+ `when`(privacyItemController.allIndicatorsAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).isEmpty()
+ }
+
+ @Test
+ fun testEnterpriseUser() {
+ val usage_enterprise = createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID)
+ )
+ `when`(permissionManager.indicatorAppOpUsageData).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.indicatorAppOpUsageData).thenReturn(listOf(usage_other))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).isEmpty()
+ }
+
+ @Test
+ fun testStartActivityCorrectIntent() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(PERM_MICROPHONE)
+ verify(activityStarter)
+ .startActivity(capture(intentCaptor), eq(true), any())
+
+ assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_APPS)
+ assertThat(intentCaptor.value.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
+ .isEqualTo(PERM_MICROPHONE)
+ }
+
+ @Test
+ fun testStartActivitySuccess() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(PERM_MICROPHONE)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
+
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testStartActivityFailure() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.starter?.invoke(PERM_MICROPHONE)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
+
+ verify(dialog, never()).dismiss()
+ }
+
+ private fun exhaustExecutors() {
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+ }
+
+ private fun setUpDefaultMockResponses() {
+ `when`(permissionManager.indicatorAppOpUsageData).thenReturn(emptyList())
+
+ `when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenAnswer {
+ FakeApplicationInfo(it.getArgument(0))
+ }
+
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+ `when`(privacyItemController.allIndicatorsAvailable).thenReturn(false)
+
+ `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 createMockPermGroupUsage(
+ packageName: String = TEST_PACKAGE_NAME,
+ uid: Int = generateUidForUser(USER_ID),
+ permGroupName: String = PERM_CAMERA,
+ lastAccess: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ attribution: CharSequence? = null
+ ): PermGroupUsage {
+ val usage = mock(PermGroupUsage::class.java)
+ `when`(usage.packageName).thenReturn(packageName)
+ `when`(usage.uid).thenReturn(uid)
+ `when`(usage.permGroupName).thenReturn(permGroupName)
+ `when`(usage.lastAccess).thenReturn(lastAccess)
+ `when`(usage.isActive).thenReturn(isActive)
+ `when`(usage.isPhoneCall).thenReturn(isPhoneCall)
+ `when`(usage.attribution).thenReturn(attribution)
+
+ return usage
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
new file mode 100644
index 0000000000000..9762ffffb02e3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogTest.kt
@@ -0,0 +1,239 @@
+/*
+ * 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
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class PrivacyDialogTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var starter: (String) -> Unit
+
+ private lateinit var dialog: PrivacyDialog
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @After
+ fun teardown() {
+ if (this::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun testStarterCalledWithCorrectPermGroupName() {
+ val list = listOf(
+ PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ "App",
+ null,
+ 0L,
+ false,
+ false,
+ false
+ )
+ )
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ dialog.requireViewById(R.id.link).callOnClick()
+ verify(starter).invoke(PrivacyType.TYPE_MICROPHONE.permGroupName)
+ }
+
+ @Test
+ fun testDismissListenerCalledOnDismiss() {
+ dialog = PrivacyDialog(context, emptyList(), starter)
+ val dismissListener = mock(PrivacyDialog.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+
+ verify(dismissListener, never()).onDialogDismissed()
+ dialog.dismiss()
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
+ dialog = PrivacyDialog(context, emptyList(), starter)
+ val dismissListener = mock(PrivacyDialog.OnDialogDismissed::class.java)
+ dialog.show()
+ dialog.dismiss()
+
+ dialog.addOnDismissListener(dismissListener)
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testCorrectNumElements() {
+ val list = listOf(
+ PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_CAMERA,
+ "App",
+ null,
+ 0L,
+ true,
+ false,
+ false
+ ),
+ PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ "App",
+ null,
+ 0L,
+ false,
+ false,
+ false
+ )
+ )
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById(R.id.root).childCount).isEqualTo(2)
+ }
+
+ @Test
+ fun testUsingText() {
+ val element = PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_CAMERA,
+ "App",
+ null,
+ 0L,
+ true,
+ false,
+ false
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById(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 = PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ "App",
+ null,
+ 0L,
+ false,
+ false,
+ false
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById(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 = PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ "App",
+ null,
+ 0L,
+ false,
+ true,
+ false
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById(R.id.text).text.toString()).contains(
+ context.getString(R.string.ongoing_privacy_dialog_enterprise)
+ )
+ }
+
+ @Test
+ fun testPhoneCall() {
+ val element = PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ "App",
+ null,
+ 0L,
+ false,
+ false,
+ true
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById(R.id.text).text.toString()).contains(
+ context.getString(R.string.ongoing_privacy_dialog_phonecall)
+ )
+ }
+
+ @Test
+ fun testAttribution() {
+ val element = PrivacyDialog.PrivacyElement(
+ PrivacyType.TYPE_MICROPHONE,
+ "App",
+ "attribution",
+ 0L,
+ false,
+ false,
+ true
+ )
+
+ val list = listOf(element)
+ dialog = PrivacyDialog(context, list, starter)
+ dialog.show()
+ assertThat(dialog.requireViewById(R.id.text).text.toString()).contains(
+ context.getString(
+ R.string.ongoing_privacy_dialog_attribution_text,
+ element.attribution
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index d1cc75708a197..97a8459161854 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.privacy.PrivacyDialogController
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.qs.carrier.QSCarrierGroup
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.NextAlarmController
import com.android.systemui.util.RingerModeTracker
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.utils.leaks.FakeZenModeController
@@ -98,6 +100,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
@Mock
private lateinit var privacyChip: OngoingPrivacyChip
@Mock
+ private lateinit var privacyDialogController: PrivacyDialogController
+ @Mock
private lateinit var clock: Clock
@Mock
private lateinit var mockView: View
@@ -114,6 +118,7 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
`when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
`when`(view.resources).thenReturn(mContext.resources)
`when`(view.isAttachedToWindow).thenReturn(true)
+ `when`(view.context).thenReturn(context)
controller = QuickStatusBarHeaderController(
view,
@@ -131,7 +136,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
quickQSPanelController,
qsCarrierGroupControllerBuilder,
privacyLogger,
- colorExtractor
+ colorExtractor,
+ privacyDialogController
)
}
@@ -222,6 +228,18 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
assertThat(captor.value).containsExactly(cameraString, micString, locationString)
}
+ @Test
+ fun testPrivacyChipClicked() {
+ controller.init()
+
+ val captor = argumentCaptor()
+ verify(privacyChip).setOnClickListener(capture(captor))
+
+ captor.value.onClick(privacyChip)
+
+ verify(privacyDialogController).showDialog(any(Context::class.java))
+ }
+
private fun stubViews() {
`when`(view.findViewById(anyInt())).thenReturn(mockView)
`when`(view.findViewById(R.id.carrier_group)).thenReturn(qsCarrierGroup)