Implement the new Privacy indicators dialog

Dialog implemented to current mocks.

Missing:
- Design
- Some a11y implementation
- Color palette for circle in icons

Test: manual
Test: SystemUITests
Fixes: 177422611
Bug: 178093014

Change-Id: Iaca8b90e4e6a2d422c46521f116b01c4b049acf1
This commit is contained in:
Fabian Kozynski
2021-01-11 15:15:29 -05:00
parent a8786df7e1
commit 503e56e87e
19 changed files with 1576 additions and 14 deletions

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?android:attr/colorBackground" />
<corners android:radius="@dimen/ongoing_appops_dialog_bg_corner_radius" />
</shape>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@id/background"
android:gravity="center"
>
<shape android:shape="oval">
<size
android:height="28dp"
android:width="28dp"
/>
<solid android:color="@color/privacy_circle_camera" />
</shape>
</item>
<item android:id="@id/icon"
android:gravity="center"
android:width="20dp"
android:height="20dp"
android:drawable="@*android:drawable/perm_group_camera"
/>
</layer-list>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@id/background"
android:gravity="center"
>
<shape android:shape="oval">
<size
android:height="28dp"
android:width="28dp"
/>
<solid android:color="@color/privacy_circle_microphone_location" />
</shape>
</item>
<item android:id="@id/icon"
android:gravity="center"
android:width="20dp"
android:height="20dp"
android:drawable="@*android:drawable/perm_group_microphone"
/>
</layer-list>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@id/background"
android:gravity="center"
>
<shape android:shape="oval">
<size
android:height="28dp"
android:width="28dp"
/>
<solid android:color="@color/privacy_circle_microphone_location" />
</shape>
</item>
<item android:id="@id/icon"
android:gravity="center"
android:width="20dp"
android:height="20dp"
android:drawable="@*android:drawable/perm_group_microphone"
/>
</layer-list>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/ongoing_appops_dialog_side_margins"
android:layout_marginEnd="@dimen/ongoing_appops_dialog_side_margins"
android:orientation="vertical"
android:padding = "8dp"
android:background="@drawable/privacy_dialog_bg"
/>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:orientation="horizontal"
android:layout_marginTop="8dp"
>
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
/>
<TextView
android:id="@+id/text"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:gravity="start | center_vertical"
android:textAppearance="@style/TextAppearance.QS.TileLabel"
/>
<FrameLayout
android:id="@+id/link"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:background="?android:attr/selectableItemBackground"
>
<ImageView
android:layout_height="24dp"
android:layout_width="24dp"
android:layout_gravity="center"
android:src="@*android:drawable/ic_chevron_end"
android:tint="?android:attr/textColorPrimary"
/>
</FrameLayout>
</LinearLayout>

View File

@@ -96,4 +96,8 @@
<!-- Color of background circle of user avatars in quick settings user switcher -->
<color name="qs_user_switcher_avatar_background">#3C4043</color>
<!-- Colors for privacy dialog. These should be changed to the new palette -->
<color name="privacy_circle_camera">#81C995</color> <!-- g300 -->
<color name="privacy_circle_microphone_location">#FCAD70</color> <!--o300 -->
</resources>

View File

@@ -266,4 +266,8 @@
<color name="misalignment_text_color">#F28B82</color>
<color name="screenrecord_status_color">#E94235</color>
<!-- TODO(b/178093014) Colors for privacy dialog. These should be changed to the new palette -->
<color name="privacy_circle_camera">#1E8E3E</color> <!-- g600 -->
<color name="privacy_circle_microphone_location">#E8710A</color> <!--o600 -->
</resources>

View File

@@ -1174,6 +1174,10 @@
<!-- Radius of Ongoing App Ops chip corners -->
<dimen name="ongoing_appops_chip_bg_corner_radius">16dp</dimen>
<dimen name="ongoing_appops_dialog_bg_corner_radius">@dimen/notification_corner_radius</dimen>
<dimen name="ongoing_appops_dialog_side_margins">@dimen/notification_shade_content_margin_horizontal</dimen>
<!-- Size of the RAT type for CellularTile -->
<dimen name="celltile_rat_type_size">10sp</dimen>

View File

@@ -2547,6 +2547,21 @@
<!-- Separator for types, before last type. Include spaces before and after if needed [CHAR LIMIT=10] -->
<string name="ongoing_privacy_dialog_last_separator">\u0020and\u0020</string>
<!-- Text for privacy dialog, using sensitive app op (one of camera, location, microphone) right now [CHAR LIMIT=NONE] -->
<string name="ongoing_privacy_dialog_using_op"><xliff:g id="application_name" example="Photos">%1$s</xliff:g> is using the <xliff:g id="app_opp_name" example="camera">%2$s</xliff:g></string>
<!-- Text for privacy dialog, recently used sensitive app op (one of camera, location, microphone) [CHAR LIMIT=NONE] -->
<string name="ongoing_privacy_dialog_recent_op"><xliff:g id="application_name" example="Photos">%1$s</xliff:g> used the <xliff:g id="app_opp_name" example="camera">%2$s</xliff:g> recently</string>
<!-- Text for privacy dialog, indicating that the application is the enterprise version [CHAR LIMIT=NONE] -->
<string name="ongoing_privacy_dialog_enterprise">(enterprise)</string>
<!-- Text for privacy dialog, identifying the phonecall app [CHAR LIMIT=NONE]-->
<string name="ongoing_privacy_dialog_phonecall">Phonecall</string>
<!-- Text for privacy dialog, indicating that an app is using an op on behalf of another [CHAR LIMIT=NONE] -->
<string name="ongoing_privacy_dialog_attribution_text">(through <xliff:g id="attribution" example="Special app">%s</xliff:g>)</string>
<!-- Text for camera app op [CHAR LIMIT=20]-->
<string name="privacy_type_camera">camera</string>

View File

@@ -54,6 +54,7 @@ import android.os.PowerManager;
import android.os.ServiceManager;
import android.os.UserManager;
import android.os.Vibrator;
import android.permission.PermissionManager;
import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
import android.telecom.TelecomManager;
@@ -379,4 +380,10 @@ public class FrameworkServicesModule {
static WindowManager provideWindowManager(Context context) {
return context.getSystemService(WindowManager.class);
}
@Provides
@Singleton
static PermissionManager providePermissionManager(Context context) {
return context.getSystemService(PermissionManager.class);
}
}

View File

@@ -0,0 +1,200 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.privacy
import android.content.Context
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.text.TextUtils
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowInsets
import android.widget.ImageView
import android.widget.TextView
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.statusbar.phone.SystemUIDialog
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
/**
* Dialog to show ongoing and recent app ops usage.
*
* @see PrivacyDialogController
* @param context A context to create the dialog
* @param list list of elements to show in the dialog. The elements will show in the same order they
* appear in the list
* @param activityStarter a callback to start an activity for a given permission group name (as
* given by [PrivacyType.permGroupName])
*/
class PrivacyDialog(
context: Context,
private val list: List<PrivacyElement>,
activityStarter: (String) -> Unit
) : SystemUIDialog(context, R.style.ScreenRecord) {
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()
setLayout(MATCH_PARENT, WRAP_CONTENT)
setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
setBackgroundDrawable(null)
}
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 onStop() {
super.onStop()
dismissed.set(true)
val iterator = dismissListeners.iterator()
while (iterator.hasNext()) {
val el = iterator.next()
iterator.remove()
el.get()?.onDialogDismissed()
}
}
private fun createView(element: PrivacyElement): View {
val newView = LayoutInflater.from(context).inflate(
R.layout.privacy_dialog_item, rootView, false
) as ViewGroup
val d = getDrawableForType(element.type)
d.findDrawableByLayerId(R.id.icon).setTint(iconColorSolid)
newView.requireViewById<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, element.type.getName(context))
val finalText = element.attribution?.let {
TextUtils.concat(
firstLine,
"\n",
context.getString(R.string.ongoing_privacy_dialog_attribution_text, it)
)
} ?: firstLine
newView.requireViewById<TextView>(R.id.text).text = finalText
newView.requireViewById<View>(R.id.link).apply {
tag = element.type.permGroupName
setOnClickListener(clickListener)
}
return newView
}
private fun getStringIdForState(active: Boolean): Int {
return if (active) {
R.string.ongoing_privacy_dialog_using_op
} else {
R.string.ongoing_privacy_dialog_recent_op
}
}
private fun getDrawableForType(type: PrivacyType): LayerDrawable {
return context.getDrawable(when (type) {
PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
}) as LayerDrawable
}
private val clickListener = View.OnClickListener { v ->
if (v.id == R.id.link) {
v.tag?.let { activityStarter(it as String) }
}
}
/** */
data class PrivacyElement(
val type: PrivacyType,
val applicationName: CharSequence,
val attribution: CharSequence?,
val lastActiveTimestamp: Long,
val active: Boolean,
val enterprise: Boolean,
val phoneCall: Boolean
) {
private val builder = StringBuilder("PrivacyElement(")
init {
builder.append("type=${type.logName}")
builder.append(", appName=$applicationName")
if (attribution != null) {
builder.append(", attribution=$attribution")
}
builder.append(", lastActive=$lastActiveTimestamp")
if (active) {
builder.append(", active")
}
if (enterprise) {
builder.append(", enterprise")
}
if (phoneCall) {
builder.append(", phoneCall")
}
builder.append(")")
}
override fun toString(): String = builder.toString()
}
/** */
interface OnDialogDismissed {
fun onDialogDismissed()
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.privacy
import android.Manifest
import android.app.ActivityManager
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserHandle
import android.permission.PermGroupUsage
import android.permission.PermissionManager
import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import java.util.concurrent.Executor
import javax.inject.Inject
private val defaultDialogProvider = object : PrivacyDialogController.DialogProvider {
override fun makeDialog(
context: Context,
list: List<PrivacyDialog.PrivacyElement>,
starter: (String) -> Unit
): PrivacyDialog {
return PrivacyDialog(context, list, starter)
}
}
/**
* Controller for [PrivacyDialog].
*
* This controller shows and dismissed the dialog, as well as determining the information to show in
* it.
*/
@SysUISingleton
class PrivacyDialogController(
private val permissionManager: PermissionManager,
private val packageManager: PackageManager,
private val privacyItemController: PrivacyItemController,
private val userTracker: UserTracker,
private val activityStarter: ActivityStarter,
private val backgroundExecutor: Executor,
private val uiExecutor: Executor,
private val privacyLogger: PrivacyLogger,
private val keyguardStateController: KeyguardStateController,
@VisibleForTesting private val dialogProvider: DialogProvider
) {
@Inject
constructor(
permissionManager: PermissionManager,
packageManager: PackageManager,
privacyItemController: PrivacyItemController,
userTracker: UserTracker,
activityStarter: ActivityStarter,
@Background backgroundExecutor: Executor,
@Main uiExecutor: Executor,
privacyLogger: PrivacyLogger,
keyguardStateController: KeyguardStateController
) : this(
permissionManager,
packageManager,
privacyItemController,
userTracker,
activityStarter,
backgroundExecutor,
uiExecutor,
privacyLogger,
keyguardStateController,
defaultDialogProvider
)
companion object {
private const val TAG = "PrivacyDialogController"
}
private var dialog: Dialog? = null
private val onDialogDismissed = object : PrivacyDialog.OnDialogDismissed {
override fun onDialogDismissed() {
privacyLogger.logPrivacyDialogDismissed()
dialog = null
}
}
@MainThread
private fun startActivity(permGroupName: String) {
val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_APPS)
intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
privacyLogger.logStartSettingsActivityFromDialog(permGroupName)
if (!keyguardStateController.isUnlocked) {
// If we are locked, hide the dialog so the user can unlock
dialog?.hide()
}
// startActivity calls internally startActivityDismissingKeyguard
activityStarter.startActivity(intent, true) {
if (ActivityManager.isStartResultSuccessful(it)) {
dismissDialog()
} else {
dialog?.show()
}
}
}
@WorkerThread
private fun permGroupUsage(): List<PermGroupUsage> = permissionManager.indicatorAppOpUsageData
/**
* Show the [PrivacyDialog]
*
* This retrieves the permission usage from [PermissionManager] and creates a new
* [PrivacyDialog] with a list of [PrivacyDialog.PrivacyElement] to show.
*
* This list will be filtered by [filterAndSelect]. Only types available by
* [PrivacyItemController] will be shown.
*
* @param context A context to use to create the dialog.
* @see filterAndSelect
*/
fun showDialog(context: Context) {
dismissDialog()
backgroundExecutor.execute {
val usage = permGroupUsage()
val userInfos = userTracker.userProfiles
privacyLogger.logUnfilteredPermGroupUsage(usage)
val items = usage.mapNotNull {
val type = filterType(permGroupToPrivacyType(it.permGroupName))
val userInfo = userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
userInfo?.let { ui ->
type?.let { t ->
// Only try to get the app name if we actually need it
val appName = if (it.isPhoneCall) {
""
} else {
getLabelForPackage(it.packageName, it.uid)
}
PrivacyDialog.PrivacyElement(
t,
appName,
it.attribution,
it.lastAccess,
it.isActive,
ui.isManagedProfile,
it.isPhoneCall
)
}
}
}
uiExecutor.execute {
val elements = filterAndSelect(items)
val d = dialogProvider.makeDialog(context, elements, this::startActivity)
d.setShowForAllUsers(true)
d.addOnDismissListener(onDialogDismissed)
d.show()
privacyLogger.logShowDialogContents(elements)
dialog = d
}
}
}
/**
* Dismisses the dialog
*/
fun dismissDialog() {
dialog?.dismiss()
}
@WorkerThread
private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
return try {
packageManager
.getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
.loadLabel(packageManager)
} catch (_: PackageManager.NameNotFoundException) {
Log.w(TAG, "Label not found for: $packageName")
packageName
}
}
private fun permGroupToPrivacyType(group: String): PrivacyType? {
return when (group) {
Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
else -> null
}
}
private fun filterType(type: PrivacyType?): PrivacyType? {
return type?.let {
if (privacyItemController.allIndicatorsAvailable) {
it
} else if ((it == PrivacyType.TYPE_CAMERA || it == PrivacyType.TYPE_MICROPHONE) &&
privacyItemController.micCameraAvailable) {
it
} else if (it == PrivacyType.TYPE_LOCATION && privacyItemController.locationAvailable) {
it
} else {
null
}
}
}
/**
* Filters the list of elements to show.
*
* * Return at most one element per [PrivacyType], sorted by the natural order of the
* [PrivacyType].
* * If there are no active usages for a type, return the most recent
* * If there are multiple active usages for a type, return the most active recent.
*/
private fun filterAndSelect(
list: List<PrivacyDialog.PrivacyElement>
): List<PrivacyDialog.PrivacyElement> {
return list.groupBy { it.type }.toSortedMap().mapNotNull { entry ->
if (entry.value.isEmpty()) {
null
} else {
val actives = entry.value.filter { it.active }
val out = if (actives.isNotEmpty()) actives else entry.value
out.maxByOrNull { it.lastActiveTimestamp }
}
}
}
/**
* Interface to create a [PrivacyDialog].
*
* Can be used to inject a mock creator.
*/
interface DialogProvider {
/**
* Create a [PrivacyDialog].
*/
fun makeDialog(
context: Context,
list: List<PrivacyDialog.PrivacyElement>,
starter: (String) -> Unit
): PrivacyDialog
}
}

View File

@@ -19,21 +19,29 @@ import com.android.systemui.R
typealias Privacy = PrivacyType
enum class PrivacyType(val nameId: Int, val iconId: Int, val logName: String) {
// This is uses the icons used by the corresponding permission groups in the AndroidManifest
enum class PrivacyType(
val nameId: Int,
val iconId: Int,
val permGroupName: String,
val logName: String
) {
// This uses the icons used by the corresponding permission groups in the AndroidManifest
TYPE_CAMERA(
R.string.privacy_type_camera,
com.android.internal.R.drawable.perm_group_camera,
android.Manifest.permission_group.CAMERA,
"camera"
),
TYPE_MICROPHONE(
R.string.privacy_type_microphone,
com.android.internal.R.drawable.perm_group_microphone,
android.Manifest.permission_group.MICROPHONE,
"microphone"
),
TYPE_LOCATION(
R.string.privacy_type_location,
com.android.internal.R.drawable.perm_group_location,
android.Manifest.permission_group.LOCATION,
"location"
);

View File

@@ -16,10 +16,12 @@
package com.android.systemui.privacy.logging
import android.permission.PermGroupUsage
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel
import com.android.systemui.log.LogMessage
import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.privacy.PrivacyDialog
import javax.inject.Inject
private const val TAG = "PrivacyLog"
@@ -85,18 +87,48 @@ class PrivacyLogger @Inject constructor(
fun logStatusBarIconsVisible(
showCamera: Boolean,
showMichrophone: Boolean,
showMicrophone: Boolean,
showLocation: Boolean
) {
log(LogLevel.INFO, {
bool1 = showCamera
bool2 = showMichrophone
bool2 = showMicrophone
bool3 = showLocation
}, {
"Status bar icons visible: camera=$bool1, microphone=$bool2, location=$bool3"
})
}
fun logUnfilteredPermGroupUsage(contents: List<PermGroupUsage>) {
log(LogLevel.DEBUG, {
str1 = contents.toString()
}, {
"Perm group usage: $str1"
})
}
fun logShowDialogContents(contents: List<PrivacyDialog.PrivacyElement>) {
log(LogLevel.INFO, {
str1 = contents.toString()
}, {
"Privacy dialog shown. Contents: $str1"
})
}
fun logPrivacyDialogDismissed() {
log(LogLevel.INFO, {}, {
"Privacy dialog dismissed"
})
}
fun logStartSettingsActivityFromDialog(permGroupName: String) {
log(LogLevel.INFO, {
str1 = permGroupName
}, {
"Start settings activity from dialog for perm group: $str1"
})
}
private inline fun log(
logLevel: LogLevel,
initializer: LogMessage.() -> Unit,

View File

@@ -20,8 +20,6 @@ import android.app.AlarmManager.AlarmClockInfo;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.AlarmClock;
import android.provider.Settings;
import android.service.notification.ZenModeConfig;
@@ -43,6 +41,7 @@ import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.privacy.OngoingPrivacyChip;
import com.android.systemui.privacy.PrivacyChipEvent;
import com.android.systemui.privacy.PrivacyDialogController;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.privacy.logging.PrivacyLogger;
@@ -94,6 +93,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
private final StatusBarIconController.TintedIconManager mIconManager;
private final DemoMode mDemoModeReceiver;
private final PrivacyLogger mPrivacyLogger;
private final PrivacyDialogController mPrivacyDialogController;
private boolean mListening;
private AlarmClockInfo mNextAlarm;
@@ -198,13 +198,8 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
}
} else if (v == mPrivacyChip) {
// If the privacy chip is visible, it means there were some indicators
Handler mUiHandler = new Handler(Looper.getMainLooper());
mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK);
mUiHandler.post(() -> {
mActivityStarter.postStartActivityDismissingKeyguard(
new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0);
mQSTileHost.collapsePanels();
});
mPrivacyDialogController.showDialog(getContext());
} else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) {
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
Settings.ACTION_SOUND_SETTINGS), 0);
@@ -222,7 +217,8 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
UserTracker userTracker, QuickQSPanelController quickQSPanelController,
QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder,
PrivacyLogger privacyLogger,
SysuiColorExtractor colorExtractor) {
SysuiColorExtractor colorExtractor,
PrivacyDialogController privacyDialogController) {
super(view);
mZenModeController = zenModeController;
mNextAlarmController = nextAlarmController;
@@ -238,6 +234,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
mLifecycle = new LifecycleRegistry(mLifecycleOwner);
mHeaderQsPanelController = quickQSPanelController;
mPrivacyLogger = privacyLogger;
mPrivacyDialogController = privacyDialogController;
mQSCarrierGroupController = qsCarrierGroupControllerBuilder
.setQSCarrierGroup(mView.findViewById(R.id.carrier_group))

View File

@@ -0,0 +1,550 @@
/*
* 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.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.UserInfo
import android.os.UserHandle
import android.permission.PermGroupUsage
import android.permission.PermissionManager
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
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.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.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class PrivacyDialogControllerTest : 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 = "test attribution"
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: PrivacyDialog
@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
@Captor
private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialog.OnDialogDismissed>
@Captor
private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
@Captor
private lateinit var intentCaptor: ArgumentCaptor<Intent>
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
private val uiExecutor = FakeExecutor(FakeSystemClock())
private lateinit var controller: PrivacyDialogController
private var nextUid: Int = 0
private val dialogProvider = object : PrivacyDialogController.DialogProvider {
var list: List<PrivacyDialog.PrivacyElement>? = null
var starter: ((String) -> Unit)? = null
override fun makeDialog(
context: Context,
list: List<PrivacyDialog.PrivacyElement>,
starter: (String) -> Unit
): PrivacyDialog {
this.list = list
this.starter = starter
return dialog
}
}
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
nextUid = 0
setUpDefaultMockResponses()
controller = PrivacyDialogController(
permissionManager,
packageManager,
privacyItemController,
userTracker,
activityStarter,
backgroundExecutor,
uiExecutor,
privacyLogger,
keyguardStateController,
dialogProvider
)
}
@After
fun tearDown() {
FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
dialogProvider.list = null
dialogProvider.starter = null
}
@Test
fun testPermissionManagerOnlyCalledInBackgroundThread() {
controller.showDialog(context)
verify(permissionManager, never()).indicatorAppOpUsageData
backgroundExecutor.runAllReady()
verify(permissionManager).indicatorAppOpUsageData
}
@Test
fun testPackageManagerOnlyCalledInBackgroundThread() {
val usage = createMockPermGroupUsage()
`when`(usage.isPhoneCall).thenReturn(false)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(listOf(usage))
controller.showDialog(context)
verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
backgroundExecutor.runAllReady()
verify(packageManager, atLeastOnce())
.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
}
@Test
fun testShowDialogShowsDialog() {
controller.showDialog(context)
exhaustExecutors()
verify(dialog).show()
}
@Test
fun testHideDialogDismissesDialogIfShown() {
controller.showDialog(context)
exhaustExecutors()
controller.dismissDialog()
verify(dialog).dismiss()
}
@Test
fun testHideDialogNoopIfNotShown() {
controller.dismissDialog()
verify(dialog, never()).dismiss()
}
@Test
fun testHideDialogNoopAfterDismissed() {
controller.showDialog(context)
exhaustExecutors()
verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
dialogDismissedCaptor.value.onDialogDismissed()
controller.dismissDialog()
verify(dialog, never()).dismiss()
}
@Test
fun testShowForAllUsers() {
controller.showDialog(context)
exhaustExecutors()
verify(dialog).setShowForAllUsers(true)
}
@Test
fun testSingleElementInList() {
val usage = createMockPermGroupUsage(
packageName = TEST_PACKAGE_NAME,
uid = generateUidForUser(0),
permGroupName = PERM_CAMERA,
lastAccess = 5L,
isActive = true,
isPhoneCall = false,
attribution = TEST_ATTRIBUTION
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(listOf(usage))
controller.showDialog(context)
exhaustExecutors()
val expected = PrivacyDialog.PrivacyElement(
type = PrivacyType.TYPE_CAMERA,
applicationName = TEST_PACKAGE_NAME,
attribution = TEST_ATTRIBUTION,
lastActiveTimestamp = 5L,
active = true,
phoneCall = false,
enterprise = false
)
assertThat(dialogProvider.list).containsExactly(expected)
}
@Test
fun testTwoElementsDifferentType_sorted() {
val usage_camera = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_camera",
permGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_microphone",
permGroupName = PERM_MICROPHONE
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_microphone, usage_camera)
)
controller.showDialog(context)
exhaustExecutors()
dialogProvider.list?.let { list ->
assertThat(list).hasSize(2)
assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
}
}
@Test
fun testTwoElementsSameType_oneActive() {
val usage_active = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_active",
isActive = true
)
val usage_recent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_recent",
isActive = false
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_recent, usage_active)
)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.active).isTrue()
}
@Test
fun testTwoElementsSameType_twoActive() {
val usage_active = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_active",
isActive = true,
lastAccess = 0L
)
val usage_active_moreRecent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_active_recent",
isActive = true,
lastAccess = 1L
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_active, usage_active_moreRecent)
)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
}
@Test
fun testManyElementsSameType_bothRecent() {
val usage_recent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_recent",
isActive = false,
lastAccess = 0L
)
val usage_moreRecent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_moreRecent",
isActive = false,
lastAccess = 1L
)
val usage_mostRecent = createMockPermGroupUsage(
packageName = "${TEST_PACKAGE_NAME}_mostRecent",
isActive = false,
lastAccess = 2L
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_recent, usage_mostRecent, usage_moreRecent)
)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
}
@Test
fun testMicAndCameraDisabled() {
val usage_camera = createMockPermGroupUsage(
permGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permGroupName = PERM_LOCATION
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(1)
assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
}
@Test
fun testLocationDisabled() {
val usage_camera = createMockPermGroupUsage(
permGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permGroupName = PERM_LOCATION
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.locationAvailable).thenReturn(false)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(2)
dialogProvider.list?.forEach {
assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
}
}
@Test
fun testAllIndicatorsAvailable() {
val usage_camera = createMockPermGroupUsage(
permGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permGroupName = PERM_LOCATION
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
`when`(privacyItemController.locationAvailable).thenReturn(false)
`when`(privacyItemController.allIndicatorsAvailable).thenReturn(true)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).hasSize(3)
}
@Test
fun testNoIndicatorsAvailable() {
val usage_camera = createMockPermGroupUsage(
permGroupName = PERM_CAMERA
)
val usage_microphone = createMockPermGroupUsage(
permGroupName = PERM_MICROPHONE
)
val usage_location = createMockPermGroupUsage(
permGroupName = PERM_LOCATION
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(
listOf(usage_camera, usage_location, usage_microphone)
)
`when`(privacyItemController.micCameraAvailable).thenReturn(false)
`when`(privacyItemController.locationAvailable).thenReturn(false)
`when`(privacyItemController.allIndicatorsAvailable).thenReturn(false)
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).isEmpty()
}
@Test
fun testEnterpriseUser() {
val usage_enterprise = createMockPermGroupUsage(
uid = generateUidForUser(ENT_USER_ID)
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(listOf(usage_enterprise))
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list?.single()?.enterprise).isTrue()
}
@Test
fun testNotCurrentUser() {
val usage_other = createMockPermGroupUsage(
uid = generateUidForUser(ENT_USER_ID + 1)
)
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(listOf(usage_other))
controller.showDialog(context)
exhaustExecutors()
assertThat(dialogProvider.list).isEmpty()
}
@Test
fun testStartActivityCorrectIntent() {
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(PERM_MICROPHONE)
verify(activityStarter)
.startActivity(capture(intentCaptor), eq(true), any<ActivityStarter.Callback>())
assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_APPS)
assertThat(intentCaptor.value.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
.isEqualTo(PERM_MICROPHONE)
}
@Test
fun testStartActivitySuccess() {
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(PERM_MICROPHONE)
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
verify(dialog).dismiss()
}
@Test
fun testStartActivityFailure() {
controller.showDialog(context)
exhaustExecutors()
dialogProvider.starter?.invoke(PERM_MICROPHONE)
verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
verify(dialog, never()).dismiss()
}
private fun exhaustExecutors() {
FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
}
private fun setUpDefaultMockResponses() {
`when`(permissionManager.indicatorAppOpUsageData).thenReturn(emptyList())
`when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
.thenAnswer {
FakeApplicationInfo(it.getArgument(0))
}
`when`(privacyItemController.locationAvailable).thenReturn(true)
`when`(privacyItemController.micCameraAvailable).thenReturn(true)
`when`(privacyItemController.allIndicatorsAvailable).thenReturn(false)
`when`(userTracker.userProfiles).thenReturn(listOf(
UserInfo(USER_ID, "", 0),
UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
))
`when`(keyguardStateController.isUnlocked).thenReturn(true)
}
private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
override fun loadLabel(pm: PackageManager): CharSequence {
return label
}
}
private fun generateUidForUser(user: Int): Int {
return user * UserHandle.PER_USER_RANGE + nextUid++
}
private fun createMockPermGroupUsage(
packageName: String = TEST_PACKAGE_NAME,
uid: Int = generateUidForUser(USER_ID),
permGroupName: String = PERM_CAMERA,
lastAccess: Long = 0L,
isActive: Boolean = false,
isPhoneCall: Boolean = false,
attribution: CharSequence? = null
): PermGroupUsage {
val usage = mock(PermGroupUsage::class.java)
`when`(usage.packageName).thenReturn(packageName)
`when`(usage.uid).thenReturn(uid)
`when`(usage.permGroupName).thenReturn(permGroupName)
`when`(usage.lastAccess).thenReturn(lastAccess)
`when`(usage.isActive).thenReturn(isActive)
`when`(usage.isPhoneCall).thenReturn(isPhoneCall)
`when`(usage.attribution).thenReturn(attribution)
return usage
}
}

View File

@@ -0,0 +1,239 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.privacy
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class PrivacyDialogTest : SysuiTestCase() {
@Mock
private lateinit var starter: (String) -> Unit
private lateinit var dialog: PrivacyDialog
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@After
fun teardown() {
if (this::dialog.isInitialized) {
dialog.dismiss()
}
}
@Test
fun testStarterCalledWithCorrectPermGroupName() {
val list = listOf(
PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
"App",
null,
0L,
false,
false,
false
)
)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
dialog.requireViewById<View>(R.id.link).callOnClick()
verify(starter).invoke(PrivacyType.TYPE_MICROPHONE.permGroupName)
}
@Test
fun testDismissListenerCalledOnDismiss() {
dialog = PrivacyDialog(context, emptyList(), starter)
val dismissListener = mock(PrivacyDialog.OnDialogDismissed::class.java)
dialog.addOnDismissListener(dismissListener)
dialog.show()
verify(dismissListener, never()).onDialogDismissed()
dialog.dismiss()
verify(dismissListener).onDialogDismissed()
}
@Test
fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
dialog = PrivacyDialog(context, emptyList(), starter)
val dismissListener = mock(PrivacyDialog.OnDialogDismissed::class.java)
dialog.show()
dialog.dismiss()
dialog.addOnDismissListener(dismissListener)
verify(dismissListener).onDialogDismissed()
}
@Test
fun testCorrectNumElements() {
val list = listOf(
PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_CAMERA,
"App",
null,
0L,
true,
false,
false
),
PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
"App",
null,
0L,
false,
false,
false
)
)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<ViewGroup>(R.id.root).childCount).isEqualTo(2)
}
@Test
fun testUsingText() {
val element = PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_CAMERA,
"App",
null,
0L,
true,
false,
false
)
val list = listOf(element)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<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 = PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
"App",
null,
0L,
false,
false,
false
)
val list = listOf(element)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<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 = PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
"App",
null,
0L,
false,
true,
false
)
val list = listOf(element)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(R.string.ongoing_privacy_dialog_enterprise)
)
}
@Test
fun testPhoneCall() {
val element = PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
"App",
null,
0L,
false,
false,
true
)
val list = listOf(element)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(R.string.ongoing_privacy_dialog_phonecall)
)
}
@Test
fun testAttribution() {
val element = PrivacyDialog.PrivacyElement(
PrivacyType.TYPE_MICROPHONE,
"App",
"attribution",
0L,
false,
false,
true
)
val list = listOf(element)
dialog = PrivacyDialog(context, list, starter)
dialog.show()
assertThat(dialog.requireViewById<TextView>(R.id.text).text.toString()).contains(
context.getString(
R.string.ongoing_privacy_dialog_attribution_text,
element.attribution
)
)
}
}

View File

@@ -27,6 +27,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyDialogController
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.qs.carrier.QSCarrierGroup
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.NextAlarmController
import com.android.systemui.util.RingerModeTracker
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.utils.leaks.FakeZenModeController
@@ -98,6 +100,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
@Mock
private lateinit var privacyChip: OngoingPrivacyChip
@Mock
private lateinit var privacyDialogController: PrivacyDialogController
@Mock
private lateinit var clock: Clock
@Mock
private lateinit var mockView: View
@@ -114,6 +118,7 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
`when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
`when`(view.resources).thenReturn(mContext.resources)
`when`(view.isAttachedToWindow).thenReturn(true)
`when`(view.context).thenReturn(context)
controller = QuickStatusBarHeaderController(
view,
@@ -131,7 +136,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
quickQSPanelController,
qsCarrierGroupControllerBuilder,
privacyLogger,
colorExtractor
colorExtractor,
privacyDialogController
)
}
@@ -222,6 +228,18 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
assertThat(captor.value).containsExactly(cameraString, micString, locationString)
}
@Test
fun testPrivacyChipClicked() {
controller.init()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
}
private fun stubViews() {
`when`(view.findViewById<View>(anyInt())).thenReturn(mockView)
`when`(view.findViewById<QSCarrierGroup>(R.id.carrier_group)).thenReturn(qsCarrierGroup)