Clone and flag enable old privacy dialog
This is to aid the review of follow up changes. Only class names were changed. And we start using the ENABLE_NEW_PRIVACY_DIALOG flag to force the old dialog only when Safety Center is on. Bug: 283456173 Test: atest SystemUITests Change-Id: I4ec29afc3d60a4d393e6b214154ea0b2e92be2e2
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,32 @@ 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 testSafetyCenterFlag() {
|
||||
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),
|
||||
@@ -179,6 +203,27 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
|
||||
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 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),
|
||||
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(privacyDialogControllerV2).showDialog(any(Context::class.java))
|
||||
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user