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:
Giulio Fiscella
2023-05-22 10:37:46 +00:00
parent 00f62accd7
commit 54649e344c
7 changed files with 1845 additions and 5 deletions

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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