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:
22
packages/SystemUI/res/drawable/privacy_dialog_bg.xml
Normal file
22
packages/SystemUI/res/drawable/privacy_dialog_bg.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
28
packages/SystemUI/res/layout/privacy_dialog.xml
Normal file
28
packages/SystemUI/res/layout/privacy_dialog.xml
Normal 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"
|
||||
/>
|
||||
62
packages/SystemUI/res/layout/privacy_dialog_item.xml
Normal file
62
packages/SystemUI/res/layout/privacy_dialog_item.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user