Add back privacy chip
This adds back the privacy chip classes (Controller and view). Change to using Executors and DeviceConfigProxy, also fix tests that were flaky before. Test: SystemUITests Bug: 160966908 Change-Id: Id3e5981a87c33a8cabe7ce348f9512d81ad2b1d8 Merged-In: Id3e5981a87c33a8cabe7ce348f9512d81ad2b1d8
This commit is contained in:
@@ -120,6 +120,13 @@ public final class SystemUiDeviceConfigFlags {
|
||||
*/
|
||||
public static final String HASH_SALT_MAX_DAYS = "hash_salt_max_days";
|
||||
|
||||
// Flag related to Privacy Indicators
|
||||
|
||||
/**
|
||||
* Whether the Permissions Hub is showing.
|
||||
*/
|
||||
public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_2_enabled";
|
||||
|
||||
// Flags related to Assistant
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,6 +81,21 @@
|
||||
<dimen name="car_keyline_2">96dp</dimen>
|
||||
<dimen name="car_keyline_3">128dp</dimen>
|
||||
|
||||
<!-- Height of icons in Ongoing App Ops dialog. Both App Op icon and application icon -->
|
||||
<dimen name="ongoing_appops_dialog_icon_height">48dp</dimen>
|
||||
<!-- Margin between text lines in Ongoing App Ops dialog -->
|
||||
<dimen name="ongoing_appops_dialog_text_margin">15dp</dimen>
|
||||
<!-- Padding around Ongoing App Ops dialog content -->
|
||||
<dimen name="ongoing_appops_dialog_content_padding">24dp</dimen>
|
||||
<!-- Margins around the Ongoing App Ops chip. In landscape, the side margins are 0 -->
|
||||
<dimen name="ongoing_appops_chip_margin">12dp</dimen>
|
||||
<!-- Start and End padding for Ongoing App Ops chip -->
|
||||
<dimen name="ongoing_appops_chip_side_padding">6dp</dimen>
|
||||
<!-- Padding between background of Ongoing App Ops chip and content -->
|
||||
<dimen name="ongoing_appops_chip_bg_padding">4dp</dimen>
|
||||
<!-- Radius of Ongoing App Ops chip corners -->
|
||||
<dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen>
|
||||
|
||||
<!-- Car volume dimens. -->
|
||||
<dimen name="car_volume_item_icon_size">@dimen/car_primary_icon_size</dimen>
|
||||
<dimen name="car_volume_item_height">@*android:dimen/car_single_line_list_item_height</dimen>
|
||||
|
||||
23
packages/SystemUI/res/drawable/privacy_chip_bg.xml
Normal file
23
packages/SystemUI/res/drawable/privacy_chip_bg.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2020 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">
|
||||
<solid android:color="#242424" /> <!-- 14% of white -->
|
||||
<padding android:paddingTop="@dimen/ongoing_appops_chip_bg_padding"
|
||||
android:paddingBottom="@dimen/ongoing_appops_chip_bg_padding" />
|
||||
<corners android:radius="@dimen/ongoing_appops_chip_bg_corner_radius" />
|
||||
</shape>
|
||||
40
packages/SystemUI/res/layout/ongoing_privacy_chip.xml
Normal file
40
packages/SystemUI/res/layout/ongoing_privacy_chip.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2020 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.
|
||||
-->
|
||||
|
||||
|
||||
<com.android.systemui.privacy.OngoingPrivacyChip
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/privacy_chip"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:focusable="true" >
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/background"
|
||||
android:layout_height="@dimen/ongoing_appops_chip_height"
|
||||
android:layout_width="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
android:layout_gravity="center_vertical">
|
||||
<LinearLayout
|
||||
android:id="@+id/icons_container"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
/>
|
||||
</FrameLayout>
|
||||
</com.android.systemui.privacy.OngoingPrivacyChip>
|
||||
@@ -14,7 +14,7 @@
|
||||
** See the License for the specific language governing permissions and
|
||||
** limitations under the License.
|
||||
-->
|
||||
<FrameLayout
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:systemui="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/quick_status_bar_system_icons"
|
||||
@@ -27,6 +27,13 @@
|
||||
android:clickable="true"
|
||||
android:paddingTop="@dimen/status_bar_padding_top" >
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical|start" >
|
||||
|
||||
<com.android.systemui.statusbar.policy.Clock
|
||||
android:id="@+id/clock"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -38,5 +45,23 @@
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
|
||||
systemui:showDark="false" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
<android.widget.Space
|
||||
android:id="@+id/space"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical|center_horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical|end" >
|
||||
|
||||
<include layout="@layout/ongoing_privacy_chip" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -510,6 +510,8 @@
|
||||
<item>com.android.systemui</item>
|
||||
</string-array>
|
||||
|
||||
<integer name="ongoing_appops_dialog_max_apps">5</integer>
|
||||
|
||||
<!-- Launcher package name for overlaying icons. -->
|
||||
<string name="launcher_overlayable_package" translatable="false">com.android.launcher3</string>
|
||||
|
||||
|
||||
@@ -1175,6 +1175,23 @@
|
||||
|
||||
<!-- How much into a DisplayCutout's bounds we can go, on each side -->
|
||||
<dimen name="display_cutout_margin_consumption">0px</dimen>
|
||||
|
||||
<!-- Height of the Ongoing App Ops chip -->
|
||||
<dimen name="ongoing_appops_chip_height">32dp</dimen>
|
||||
<!-- Padding between background of Ongoing App Ops chip and content -->
|
||||
<dimen name="ongoing_appops_chip_bg_padding">8dp</dimen>
|
||||
<!-- Side padding between background of Ongoing App Ops chip and content -->
|
||||
<dimen name="ongoing_appops_chip_side_padding">8dp</dimen>
|
||||
<!-- Margin between icons of Ongoing App Ops chip when QQS-->
|
||||
<dimen name="ongoing_appops_chip_icon_margin_collapsed">0dp</dimen>
|
||||
<!-- Margin between icons of Ongoing App Ops chip when QS-->
|
||||
<dimen name="ongoing_appops_chip_icon_margin_expanded">2dp</dimen>
|
||||
<!-- Icon size of Ongoing App Ops chip -->
|
||||
<dimen name="ongoing_appops_chip_icon_size">@dimen/status_bar_icon_drawing_size</dimen>
|
||||
<!-- Radius of Ongoing App Ops chip corners -->
|
||||
<dimen name="ongoing_appops_chip_bg_corner_radius">16dp</dimen>
|
||||
|
||||
|
||||
<!-- How much each bubble is elevated. -->
|
||||
<dimen name="bubble_elevation">1dp</dimen>
|
||||
<!-- How much the bubble flyout text container is elevated. -->
|
||||
|
||||
@@ -2608,6 +2608,27 @@
|
||||
app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
|
||||
<string name="heap_dump_tile_name">Dump SysUI Heap</string>
|
||||
|
||||
<!-- Content description for ongoing privacy chip. Use with a single app [CHAR LIMIT=NONE]-->
|
||||
<string name="ongoing_privacy_chip_content_single_app"><xliff:g id="app" example="Example App">%1$s</xliff:g> is using your <xliff:g id="types_list" example="camera, location">%2$s</xliff:g>.</string>
|
||||
|
||||
<!-- Content description for ongoing privacy chip. Use with multiple apps [CHAR LIMIT=NONE]-->
|
||||
<string name="ongoing_privacy_chip_content_multiple_apps">Applications are using your <xliff:g id="types_list" example="camera, location">%s</xliff:g>.</string>
|
||||
|
||||
<!-- Separator for types. Include spaces before and after if needed [CHAR LIMIT=10] -->
|
||||
<string name="ongoing_privacy_dialog_separator">,\u0020</string>
|
||||
|
||||
<!-- 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 camera app op [CHAR LIMIT=20]-->
|
||||
<string name="privacy_type_camera">camera</string>
|
||||
|
||||
<!-- Text for location app op [CHAR LIMIT=20]-->
|
||||
<string name="privacy_type_location">location</string>
|
||||
|
||||
<!-- Text for microphone app op [CHAR LIMIT=20]-->
|
||||
<string name="privacy_type_microphone">microphone</string>
|
||||
|
||||
<!-- Text for the quick setting tile for sensor privacy [CHAR LIMIT=30] -->
|
||||
<string name="sensor_privacy_mode">Sensors off</string>
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import com.android.systemui.plugins.VolumeDialogController;
|
||||
import com.android.systemui.plugins.statusbar.StatusBarStateController;
|
||||
import com.android.systemui.power.EnhancedEstimates;
|
||||
import com.android.systemui.power.PowerUI;
|
||||
import com.android.systemui.privacy.PrivacyItemController;
|
||||
import com.android.systemui.recents.OverviewProxyService;
|
||||
import com.android.systemui.recents.Recents;
|
||||
import com.android.systemui.screenrecord.RecordingController;
|
||||
@@ -294,6 +295,7 @@ public class Dependency {
|
||||
@Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager;
|
||||
@Inject Lazy<AutoHideController> mAutoHideController;
|
||||
@Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener;
|
||||
@Inject Lazy<PrivacyItemController> mPrivacyItemController;
|
||||
@Inject @Background Lazy<Looper> mBgLooper;
|
||||
@Inject @Background Lazy<Handler> mBgHandler;
|
||||
@Inject @Main Lazy<Looper> mMainLooper;
|
||||
@@ -491,6 +493,7 @@ public class Dependency {
|
||||
mProviders.put(ForegroundServiceNotificationListener.class,
|
||||
mForegroundServiceNotificationListener::get);
|
||||
mProviders.put(ClockManager.class, mClockManager::get);
|
||||
mProviders.put(PrivacyItemController.class, mPrivacyItemController::get);
|
||||
mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get);
|
||||
mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get);
|
||||
mProviders.put(PackageManagerWrapper.class, mPackageManagerWrapper::get);
|
||||
|
||||
@@ -57,7 +57,6 @@ public class AppOpsControllerImpl implements AppOpsController,
|
||||
private static final long NOTED_OP_TIME_DELAY_MS = 5000;
|
||||
private static final String TAG = "AppOpsControllerImpl";
|
||||
private static final boolean DEBUG = false;
|
||||
private final Context mContext;
|
||||
|
||||
private final AppOpsManager mAppOps;
|
||||
private H mBGHandler;
|
||||
@@ -83,7 +82,6 @@ public class AppOpsControllerImpl implements AppOpsController,
|
||||
Context context,
|
||||
@Background Looper bgLooper,
|
||||
DumpManager dumpManager) {
|
||||
mContext = context;
|
||||
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
|
||||
mBGHandler = new H(bgLooper);
|
||||
final int numOps = OPS.length;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import com.android.systemui.R
|
||||
|
||||
class OngoingPrivacyChip @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttrs: Int = 0,
|
||||
defStyleRes: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
|
||||
|
||||
private val iconMarginExpanded = context.resources.getDimensionPixelSize(
|
||||
R.dimen.ongoing_appops_chip_icon_margin_expanded)
|
||||
private val iconMarginCollapsed = context.resources.getDimensionPixelSize(
|
||||
R.dimen.ongoing_appops_chip_icon_margin_collapsed)
|
||||
private val iconSize =
|
||||
context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_size)
|
||||
private val iconColor = context.resources.getColor(
|
||||
R.color.status_bar_clock_color, context.theme)
|
||||
private val sidePadding =
|
||||
context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
|
||||
private val backgroundDrawable = context.getDrawable(R.drawable.privacy_chip_bg)
|
||||
private lateinit var iconsContainer: LinearLayout
|
||||
private lateinit var back: FrameLayout
|
||||
var expanded = false
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
updateView()
|
||||
}
|
||||
}
|
||||
|
||||
var builder = PrivacyChipBuilder(context, emptyList<PrivacyItem>())
|
||||
var privacyList = emptyList<PrivacyItem>()
|
||||
set(value) {
|
||||
field = value
|
||||
builder = PrivacyChipBuilder(context, value)
|
||||
updateView()
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
|
||||
back = requireViewById(R.id.background)
|
||||
iconsContainer = requireViewById(R.id.icons_container)
|
||||
}
|
||||
|
||||
// Should only be called if the builder icons or app changed
|
||||
private fun updateView() {
|
||||
back.background = if (expanded) backgroundDrawable else null
|
||||
val padding = if (expanded) sidePadding else 0
|
||||
back.setPaddingRelative(padding, 0, padding, 0)
|
||||
fun setIcons(chipBuilder: PrivacyChipBuilder, iconsContainer: ViewGroup) {
|
||||
iconsContainer.removeAllViews()
|
||||
chipBuilder.generateIcons().forEachIndexed { i, it ->
|
||||
it.mutate()
|
||||
it.setTint(iconColor)
|
||||
val image = ImageView(context).apply {
|
||||
setImageDrawable(it)
|
||||
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||
}
|
||||
iconsContainer.addView(image, iconSize, iconSize)
|
||||
if (i != 0) {
|
||||
val lp = image.layoutParams as MarginLayoutParams
|
||||
lp.marginStart = if (expanded) iconMarginExpanded else iconMarginCollapsed
|
||||
image.layoutParams = lp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!privacyList.isEmpty()) {
|
||||
generateContentDescription()
|
||||
setIcons(builder, iconsContainer)
|
||||
val lp = iconsContainer.layoutParams as FrameLayout.LayoutParams
|
||||
lp.gravity = Gravity.CENTER_VERTICAL or
|
||||
(if (expanded) Gravity.CENTER_HORIZONTAL else Gravity.END)
|
||||
iconsContainer.layoutParams = lp
|
||||
} else {
|
||||
iconsContainer.removeAllViews()
|
||||
}
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun generateContentDescription() {
|
||||
val typesText = builder.joinTypes()
|
||||
contentDescription = context.getString(
|
||||
R.string.ongoing_privacy_chip_content_multiple_apps, typesText)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.systemui.R
|
||||
|
||||
class PrivacyChipBuilder(private val context: Context, itemsList: List<PrivacyItem>) {
|
||||
|
||||
val appsAndTypes: List<Pair<PrivacyApplication, List<PrivacyType>>>
|
||||
val types: List<PrivacyType>
|
||||
private val separator = context.getString(R.string.ongoing_privacy_dialog_separator)
|
||||
private val lastSeparator = context.getString(R.string.ongoing_privacy_dialog_last_separator)
|
||||
|
||||
init {
|
||||
appsAndTypes = itemsList.groupBy({ it.application }, { it.privacyType })
|
||||
.toList()
|
||||
.sortedWith(compareBy({ -it.second.size }, // Sort by number of AppOps
|
||||
{ it.second.min() })) // Sort by "smallest" AppOpp (Location is largest)
|
||||
types = itemsList.map { it.privacyType }.distinct().sorted()
|
||||
}
|
||||
|
||||
fun generateIcons() = types.map { it.getIcon(context) }
|
||||
|
||||
private fun <T> List<T>.joinWithAnd(): StringBuilder {
|
||||
return subList(0, size - 1).joinTo(StringBuilder(), separator = separator).apply {
|
||||
append(lastSeparator)
|
||||
append(this@joinWithAnd.last())
|
||||
}
|
||||
}
|
||||
|
||||
fun joinTypes(): String {
|
||||
return when (types.size) {
|
||||
0 -> ""
|
||||
1 -> types[0].getName(context)
|
||||
else -> types.map { it.getName(context) }.joinWithAnd().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.internal.logging.UiEvent
|
||||
import com.android.internal.logging.UiEventLogger
|
||||
|
||||
enum class PrivacyChipEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
|
||||
@UiEvent(doc = "Privacy chip is viewed by the user. Logged at most once per time QS is visible")
|
||||
ONGOING_INDICATORS_CHIP_VIEW(601),
|
||||
|
||||
@UiEvent(doc = "Privacy chip is clicked")
|
||||
ONGOING_INDICATORS_CHIP_CLICK(602);
|
||||
|
||||
override fun getId() = _id
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 com.android.systemui.R
|
||||
|
||||
typealias Privacy = PrivacyType
|
||||
|
||||
enum class PrivacyType(val nameId: Int, val iconId: Int) {
|
||||
// This is 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),
|
||||
TYPE_MICROPHONE(R.string.privacy_type_microphone,
|
||||
com.android.internal.R.drawable.perm_group_microphone),
|
||||
TYPE_LOCATION(R.string.privacy_type_location,
|
||||
com.android.internal.R.drawable.perm_group_location);
|
||||
|
||||
fun getName(context: Context) = context.resources.getString(nameId)
|
||||
|
||||
fun getIcon(context: Context) = context.resources.getDrawable(iconId, context.theme)
|
||||
}
|
||||
|
||||
data class PrivacyItem(val privacyType: PrivacyType, val application: PrivacyApplication)
|
||||
|
||||
data class PrivacyApplication(val packageName: String, val uid: Int)
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.app.AppOpsManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.DeviceConfig
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
||||
import com.android.systemui.Dumpable
|
||||
import com.android.systemui.appops.AppOpItem
|
||||
import com.android.systemui.appops.AppOpsController
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.dagger.qualifiers.Background
|
||||
import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.util.DeviceConfigProxy
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
import java.io.FileDescriptor
|
||||
import java.io.PrintWriter
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PrivacyItemController @Inject constructor(
|
||||
context: Context,
|
||||
private val appOpsController: AppOpsController,
|
||||
@Main uiExecutor: DelayableExecutor,
|
||||
@Background private val bgExecutor: Executor,
|
||||
private val broadcastDispatcher: BroadcastDispatcher,
|
||||
private val deviceConfigProxy: DeviceConfigProxy,
|
||||
dumpManager: DumpManager
|
||||
) : Dumpable {
|
||||
|
||||
@VisibleForTesting
|
||||
internal companion object {
|
||||
val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
|
||||
AppOpsManager.OP_RECORD_AUDIO,
|
||||
AppOpsManager.OP_COARSE_LOCATION,
|
||||
AppOpsManager.OP_FINE_LOCATION)
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_USER_SWITCHED)
|
||||
addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
|
||||
addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
|
||||
}
|
||||
const val TAG = "PrivacyItemController"
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal var privacyList = emptyList<PrivacyItem>()
|
||||
@Synchronized get() = field.toList() // Returns a shallow copy of the list
|
||||
@Synchronized set
|
||||
|
||||
fun isPermissionsHubEnabled(): Boolean {
|
||||
return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
|
||||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
|
||||
}
|
||||
|
||||
private val userManager = context.getSystemService(UserManager::class.java)
|
||||
private var currentUserIds = emptyList<Int>()
|
||||
private var listening = false
|
||||
private val callbacks = mutableListOf<WeakReference<Callback>>()
|
||||
private val internalUiExecutor = MyExecutor(WeakReference(this), uiExecutor)
|
||||
|
||||
private val notifyChanges = Runnable {
|
||||
val list = privacyList
|
||||
callbacks.forEach { it.get()?.privacyChanged(list) }
|
||||
}
|
||||
|
||||
private val updateListAndNotifyChanges = Runnable {
|
||||
updatePrivacyList()
|
||||
uiExecutor.execute(notifyChanges)
|
||||
}
|
||||
|
||||
private var indicatorsAvailable = isPermissionsHubEnabled()
|
||||
@VisibleForTesting
|
||||
internal val devicePropertiesChangedListener =
|
||||
object : DeviceConfig.OnPropertiesChangedListener {
|
||||
override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
|
||||
if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) &&
|
||||
properties.getKeyset().contains(
|
||||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) {
|
||||
indicatorsAvailable = properties.getBoolean(
|
||||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
|
||||
internalUiExecutor.updateListeningState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val cb = object : AppOpsController.Callback {
|
||||
override fun onActiveStateChanged(
|
||||
code: Int,
|
||||
uid: Int,
|
||||
packageName: String,
|
||||
active: Boolean
|
||||
) {
|
||||
val userId = UserHandle.getUserId(uid)
|
||||
if (userId in currentUserIds) {
|
||||
update(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal var userSwitcherReceiver = Receiver()
|
||||
set(value) {
|
||||
unregisterReceiver()
|
||||
field = value
|
||||
if (listening) registerReceiver()
|
||||
}
|
||||
|
||||
init {
|
||||
deviceConfigProxy.addOnPropertiesChangedListener(
|
||||
DeviceConfig.NAMESPACE_PRIVACY,
|
||||
uiExecutor,
|
||||
devicePropertiesChangedListener)
|
||||
dumpManager.registerDumpable(TAG, this)
|
||||
}
|
||||
|
||||
private fun unregisterReceiver() {
|
||||
broadcastDispatcher.unregisterReceiver(userSwitcherReceiver)
|
||||
}
|
||||
|
||||
private fun registerReceiver() {
|
||||
broadcastDispatcher.registerReceiver(userSwitcherReceiver, intentFilter,
|
||||
null /* handler */, UserHandle.ALL)
|
||||
}
|
||||
|
||||
private fun update(updateUsers: Boolean) {
|
||||
bgExecutor.execute {
|
||||
if (updateUsers) {
|
||||
val currentUser = ActivityManager.getCurrentUser()
|
||||
currentUserIds = userManager.getProfiles(currentUser).map { it.id }
|
||||
}
|
||||
updateListAndNotifyChanges.run()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates listening status based on whether there are callbacks and the indicators are enabled
|
||||
*
|
||||
* This is only called from private (add/remove)Callback and from the config listener, all in
|
||||
* main thread.
|
||||
*/
|
||||
private fun setListeningState() {
|
||||
val listen = !callbacks.isEmpty() and indicatorsAvailable
|
||||
if (listening == listen) return
|
||||
listening = listen
|
||||
if (listening) {
|
||||
appOpsController.addCallback(OPS, cb)
|
||||
registerReceiver()
|
||||
update(true)
|
||||
} else {
|
||||
appOpsController.removeCallback(OPS, cb)
|
||||
unregisterReceiver()
|
||||
// Make sure that we remove all indicators and notify listeners if we are not
|
||||
// listening anymore due to indicators being disabled
|
||||
update(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCallback(callback: WeakReference<Callback>) {
|
||||
callbacks.add(callback)
|
||||
if (callbacks.isNotEmpty() && !listening) {
|
||||
internalUiExecutor.updateListeningState()
|
||||
}
|
||||
// Notify this callback if we didn't set to listening
|
||||
else if (listening) {
|
||||
internalUiExecutor.execute(NotifyChangesToCallback(callback.get(), privacyList))
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeCallback(callback: WeakReference<Callback>) {
|
||||
// Removes also if the callback is null
|
||||
callbacks.removeIf { it.get()?.equals(callback.get()) ?: true }
|
||||
if (callbacks.isEmpty()) {
|
||||
internalUiExecutor.updateListeningState()
|
||||
}
|
||||
}
|
||||
|
||||
fun addCallback(callback: Callback) {
|
||||
internalUiExecutor.addCallback(callback)
|
||||
}
|
||||
|
||||
fun removeCallback(callback: Callback) {
|
||||
internalUiExecutor.removeCallback(callback)
|
||||
}
|
||||
|
||||
private fun updatePrivacyList() {
|
||||
if (!listening) {
|
||||
privacyList = emptyList()
|
||||
return
|
||||
}
|
||||
val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
|
||||
.mapNotNull { toPrivacyItem(it) }.distinct()
|
||||
privacyList = list
|
||||
}
|
||||
|
||||
private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
|
||||
val type: PrivacyType = when (appOpItem.code) {
|
||||
AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
|
||||
AppOpsManager.OP_COARSE_LOCATION -> PrivacyType.TYPE_LOCATION
|
||||
AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
|
||||
AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
|
||||
else -> return null
|
||||
}
|
||||
val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
|
||||
return PrivacyItem(type, app)
|
||||
}
|
||||
|
||||
// Used by containing class to get notified of changes
|
||||
interface Callback {
|
||||
fun privacyChanged(privacyItems: List<PrivacyItem>)
|
||||
}
|
||||
|
||||
internal inner class Receiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intentFilter.hasAction(intent.action)) {
|
||||
update(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NotifyChangesToCallback(
|
||||
private val callback: Callback?,
|
||||
private val list: List<PrivacyItem>
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
callback?.privacyChanged(list)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
|
||||
pw.println("PrivacyItemController state:")
|
||||
pw.println(" Listening: $listening")
|
||||
pw.println(" Current user ids: $currentUserIds")
|
||||
pw.println(" Privacy Items:")
|
||||
privacyList.forEach {
|
||||
pw.print(" ")
|
||||
pw.println(it.toString())
|
||||
}
|
||||
pw.println(" Callbacks:")
|
||||
callbacks.forEach {
|
||||
it.get()?.let {
|
||||
pw.print(" ")
|
||||
pw.println(it.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MyExecutor(
|
||||
private val outerClass: WeakReference<PrivacyItemController>,
|
||||
private val delegate: DelayableExecutor
|
||||
) : Executor {
|
||||
|
||||
private var listeningCanceller: Runnable? = null
|
||||
|
||||
override fun execute(command: Runnable) {
|
||||
delegate.execute(command)
|
||||
}
|
||||
|
||||
fun updateListeningState() {
|
||||
listeningCanceller?.run()
|
||||
listeningCanceller = delegate.executeDelayed({
|
||||
outerClass.get()?.setListeningState()
|
||||
}, 0L)
|
||||
}
|
||||
|
||||
fun addCallback(callback: Callback) {
|
||||
outerClass.get()?.addCallback(WeakReference(callback))
|
||||
}
|
||||
|
||||
fun removeCallback(callback: Callback) {
|
||||
outerClass.get()?.removeCallback(WeakReference(callback))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.AlarmClock;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.provider.Settings;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
import android.text.format.DateUtils;
|
||||
@@ -46,7 +48,9 @@ import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -55,6 +59,8 @@ import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LifecycleRegistry;
|
||||
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
|
||||
import com.android.internal.logging.UiEventLogger;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.systemui.BatteryMeterView;
|
||||
import com.android.systemui.DualToneHandler;
|
||||
@@ -63,6 +69,11 @@ import com.android.systemui.R;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.plugins.DarkIconDispatcher;
|
||||
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
|
||||
import com.android.systemui.privacy.OngoingPrivacyChip;
|
||||
import com.android.systemui.privacy.PrivacyChipBuilder;
|
||||
import com.android.systemui.privacy.PrivacyChipEvent;
|
||||
import com.android.systemui.privacy.PrivacyItem;
|
||||
import com.android.systemui.privacy.PrivacyItemController;
|
||||
import com.android.systemui.qs.QSDetail.Callback;
|
||||
import com.android.systemui.qs.carrier.QSCarrierGroup;
|
||||
import com.android.systemui.statusbar.CommandQueue;
|
||||
@@ -101,7 +112,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0;
|
||||
public static final int MAX_TOOLTIP_SHOWN_COUNT = 2;
|
||||
|
||||
private final Handler mHandler = new Handler();
|
||||
private final NextAlarmController mAlarmController;
|
||||
private final ZenModeController mZenController;
|
||||
private final StatusBarIconController mStatusBarIconController;
|
||||
@@ -140,9 +150,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
private View mRingerContainer;
|
||||
private Clock mClockView;
|
||||
private DateView mDateView;
|
||||
private OngoingPrivacyChip mPrivacyChip;
|
||||
private Space mSpace;
|
||||
private BatteryMeterView mBatteryRemainingIcon;
|
||||
private RingerModeTracker mRingerModeTracker;
|
||||
private boolean mPermissionsHubEnabled;
|
||||
|
||||
private PrivacyItemController mPrivacyItemController;
|
||||
private final UiEventLogger mUiEventLogger;
|
||||
// Used for RingerModeTracker
|
||||
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
|
||||
|
||||
@@ -156,22 +171,49 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
private int mCutOutPaddingRight;
|
||||
private float mExpandedHeaderAlpha = 1.0f;
|
||||
private float mKeyguardExpansionFraction;
|
||||
private boolean mPrivacyChipLogged = false;
|
||||
|
||||
private final DeviceConfig.OnPropertiesChangedListener mPropertiesListener =
|
||||
new DeviceConfig.OnPropertiesChangedListener() {
|
||||
@Override
|
||||
public void onPropertiesChanged(DeviceConfig.Properties properties) {
|
||||
if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace())
|
||||
&& properties.getKeyset()
|
||||
.contains(SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) {
|
||||
mPermissionsHubEnabled = properties.getBoolean(
|
||||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false);
|
||||
StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
|
||||
iconContainer.setIgnoredSlots(getIgnoredIconSlots());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() {
|
||||
@Override
|
||||
public void privacyChanged(List<PrivacyItem> privacyItems) {
|
||||
mPrivacyChip.setPrivacyList(privacyItems);
|
||||
setChipVisibility(!privacyItems.isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
@Inject
|
||||
public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
|
||||
NextAlarmController nextAlarmController, ZenModeController zenModeController,
|
||||
StatusBarIconController statusBarIconController,
|
||||
ActivityStarter activityStarter,
|
||||
CommandQueue commandQueue, RingerModeTracker ringerModeTracker) {
|
||||
ActivityStarter activityStarter, PrivacyItemController privacyItemController,
|
||||
CommandQueue commandQueue, RingerModeTracker ringerModeTracker,
|
||||
UiEventLogger uiEventLogger) {
|
||||
super(context, attrs);
|
||||
mAlarmController = nextAlarmController;
|
||||
mZenController = zenModeController;
|
||||
mStatusBarIconController = statusBarIconController;
|
||||
mActivityStarter = activityStarter;
|
||||
mPrivacyItemController = privacyItemController;
|
||||
mDualToneHandler = new DualToneHandler(
|
||||
new ContextThemeWrapper(context, R.style.QSHeaderTheme));
|
||||
mCommandQueue = commandQueue;
|
||||
mRingerModeTracker = ringerModeTracker;
|
||||
mUiEventLogger = uiEventLogger;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -198,8 +240,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
mRingerModeTextView = findViewById(R.id.ringer_mode_text);
|
||||
mRingerContainer = findViewById(R.id.ringer_container);
|
||||
mRingerContainer.setOnClickListener(this::onClick);
|
||||
mPrivacyChip = findViewById(R.id.privacy_chip);
|
||||
mPrivacyChip.setOnClickListener(this::onClick);
|
||||
mCarrierGroup = findViewById(R.id.carrier_group);
|
||||
|
||||
|
||||
updateResources();
|
||||
|
||||
Rect tintArea = new Rect(0, 0, 0, 0);
|
||||
@@ -219,6 +264,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
mClockView = findViewById(R.id.clock);
|
||||
mClockView.setOnClickListener(this);
|
||||
mDateView = findViewById(R.id.date);
|
||||
mSpace = findViewById(R.id.space);
|
||||
|
||||
// Tint for the battery icons are handled in setupHost()
|
||||
mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon);
|
||||
@@ -229,6 +275,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
|
||||
mRingerModeTextView.setSelected(true);
|
||||
mNextAlarmTextView.setSelected(true);
|
||||
|
||||
mPermissionsHubEnabled = mPrivacyItemController.isPermissionsHubEnabled();
|
||||
}
|
||||
|
||||
public QuickQSPanel getHeaderQsPanel() {
|
||||
@@ -241,6 +289,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
com.android.internal.R.string.status_bar_camera));
|
||||
ignored.add(mContext.getResources().getString(
|
||||
com.android.internal.R.string.status_bar_microphone));
|
||||
if (mPermissionsHubEnabled) {
|
||||
ignored.add(mContext.getResources().getString(
|
||||
com.android.internal.R.string.status_bar_location));
|
||||
}
|
||||
|
||||
return ignored;
|
||||
}
|
||||
@@ -256,6 +308,20 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
}
|
||||
}
|
||||
|
||||
private void setChipVisibility(boolean chipVisible) {
|
||||
if (chipVisible && mPermissionsHubEnabled) {
|
||||
mPrivacyChip.setVisibility(View.VISIBLE);
|
||||
// Makes sure that the chip is logged as viewed at most once each time QS is opened
|
||||
// mListening makes sure that the callback didn't return after the user closed QS
|
||||
if (!mPrivacyChipLogged && mListening) {
|
||||
mPrivacyChipLogged = true;
|
||||
mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW);
|
||||
}
|
||||
} else {
|
||||
mPrivacyChip.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updateRingerStatus() {
|
||||
boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
|
||||
CharSequence originalRingerText = mRingerModeTextView.getText();
|
||||
@@ -363,6 +429,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
|
||||
updateStatusIconAlphaAnimator();
|
||||
updateHeaderTextContainerAlphaAnimator();
|
||||
updatePrivacyChipAlphaAnimator();
|
||||
}
|
||||
|
||||
private void updateStatusIconAlphaAnimator() {
|
||||
@@ -377,6 +444,12 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
.build();
|
||||
}
|
||||
|
||||
private void updatePrivacyChipAlphaAnimator() {
|
||||
mPrivacyChipAlphaAnimator = new TouchAnimator.Builder()
|
||||
.addFloat(mPrivacyChip, "alpha", 1, 0, 1)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setExpanded(boolean expanded) {
|
||||
if (mExpanded == expanded) return;
|
||||
mExpanded = expanded;
|
||||
@@ -415,6 +488,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
mHeaderTextContainerView.setVisibility(INVISIBLE);
|
||||
}
|
||||
}
|
||||
if (mPrivacyChipAlphaAnimator != null) {
|
||||
mPrivacyChip.setExpanded(expansionFraction > 0.5);
|
||||
mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction);
|
||||
}
|
||||
if (expansionFraction < 1 && expansionFraction > 0.99) {
|
||||
if (mHeaderQsPanel.switchTileLayout()) {
|
||||
updateResources();
|
||||
@@ -442,6 +519,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
});
|
||||
mStatusBarIconController.addIconGroup(mIconManager);
|
||||
requestApplyInsets();
|
||||
// Change the ignored slots when DeviceConfig flag changes
|
||||
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
|
||||
mContext.getMainExecutor(), mPropertiesListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -453,6 +533,31 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
Pair<Integer, Integer> padding =
|
||||
StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner(
|
||||
cutout, cornerCutoutPadding, -1);
|
||||
if (padding == null) {
|
||||
mSystemIconsView.setPaddingRelative(
|
||||
getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0,
|
||||
getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0);
|
||||
} else {
|
||||
mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
|
||||
|
||||
}
|
||||
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams();
|
||||
boolean cornerCutout = cornerCutoutPadding != null
|
||||
&& (cornerCutoutPadding.first == 0 || cornerCutoutPadding.second == 0);
|
||||
if (cutout != null) {
|
||||
Rect topCutout = cutout.getBoundingRectTop();
|
||||
if (topCutout.isEmpty() || cornerCutout) {
|
||||
mHasTopCutout = false;
|
||||
lp.width = 0;
|
||||
mSpace.setVisibility(View.GONE);
|
||||
} else {
|
||||
mHasTopCutout = true;
|
||||
lp.width = topCutout.width();
|
||||
mSpace.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
mSpace.setLayoutParams(lp);
|
||||
setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE);
|
||||
mCutOutPaddingLeft = padding.first;
|
||||
mCutOutPaddingRight = padding.second;
|
||||
mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top;
|
||||
@@ -496,6 +601,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
setListening(false);
|
||||
mRingerModeTracker.getRingerModeInternal().removeObservers(this);
|
||||
mStatusBarIconController.removeIconGroup(mIconManager);
|
||||
DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener);
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@@ -513,10 +619,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
mZenController.addCallback(this);
|
||||
mAlarmController.addCallback(this);
|
||||
mLifecycle.setCurrentState(Lifecycle.State.RESUMED);
|
||||
mPrivacyItemController.addCallback(mPICCallback);
|
||||
} else {
|
||||
mZenController.removeCallback(this);
|
||||
mAlarmController.removeCallback(this);
|
||||
mLifecycle.setCurrentState(Lifecycle.State.CREATED);
|
||||
mPrivacyItemController.removeCallback(mPICCallback);
|
||||
mPrivacyChipLogged = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +643,17 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
|
||||
AlarmClock.ACTION_SHOW_ALARMS), 0);
|
||||
}
|
||||
} else if (v == mPrivacyChip) {
|
||||
// Makes sure that the builder is grabbed as soon as the chip is pressed
|
||||
PrivacyChipBuilder builder = mPrivacyChip.getBuilder();
|
||||
if (builder.getAppsAndTypes().size() == 0) return;
|
||||
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);
|
||||
mHost.collapsePanels();
|
||||
});
|
||||
} else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) {
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
|
||||
Settings.ACTION_SOUND_SETTINGS), 0);
|
||||
|
||||
@@ -47,6 +47,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
|
||||
import com.android.systemui.dagger.qualifiers.DisplayId;
|
||||
import com.android.systemui.dagger.qualifiers.Main;
|
||||
import com.android.systemui.dagger.qualifiers.UiBackground;
|
||||
import com.android.systemui.privacy.PrivacyItem;
|
||||
import com.android.systemui.privacy.PrivacyItemController;
|
||||
import com.android.systemui.privacy.PrivacyType;
|
||||
import com.android.systemui.qs.tiles.DndTile;
|
||||
import com.android.systemui.qs.tiles.RotationLockTile;
|
||||
import com.android.systemui.screenrecord.RecordingController;
|
||||
@@ -70,6 +73,9 @@ import com.android.systemui.statusbar.policy.ZenModeController;
|
||||
import com.android.systemui.util.RingerModeTracker;
|
||||
import com.android.systemui.util.time.DateFormatUtil;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -87,13 +93,13 @@ public class PhoneStatusBarPolicy
|
||||
ZenModeController.Callback,
|
||||
DeviceProvisionedListener,
|
||||
KeyguardStateController.Callback,
|
||||
PrivacyItemController.Callback,
|
||||
LocationController.LocationChangeCallback,
|
||||
RecordingController.RecordingStateChangeCallback {
|
||||
private static final String TAG = "PhoneStatusBarPolicy";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
static final int LOCATION_STATUS_ICON_ID =
|
||||
com.android.internal.R.drawable.perm_group_location;
|
||||
static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId();
|
||||
|
||||
private final String mSlotCast;
|
||||
private final String mSlotHotspot;
|
||||
@@ -107,6 +113,8 @@ public class PhoneStatusBarPolicy
|
||||
private final String mSlotHeadset;
|
||||
private final String mSlotDataSaver;
|
||||
private final String mSlotLocation;
|
||||
private final String mSlotMicrophone;
|
||||
private final String mSlotCamera;
|
||||
private final String mSlotSensorsOff;
|
||||
private final String mSlotScreenRecord;
|
||||
private final int mDisplayId;
|
||||
@@ -132,6 +140,7 @@ public class PhoneStatusBarPolicy
|
||||
private final DeviceProvisionedController mProvisionedController;
|
||||
private final KeyguardStateController mKeyguardStateController;
|
||||
private final LocationController mLocationController;
|
||||
private final PrivacyItemController mPrivacyItemController;
|
||||
private final Executor mUiBgExecutor;
|
||||
private final SensorPrivacyController mSensorPrivacyController;
|
||||
private final RecordingController mRecordingController;
|
||||
@@ -162,7 +171,8 @@ public class PhoneStatusBarPolicy
|
||||
RecordingController recordingController,
|
||||
@Nullable TelecomManager telecomManager, @DisplayId int displayId,
|
||||
@Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
|
||||
RingerModeTracker ringerModeTracker) {
|
||||
RingerModeTracker ringerModeTracker,
|
||||
PrivacyItemController privacyItemController) {
|
||||
mIconController = iconController;
|
||||
mCommandQueue = commandQueue;
|
||||
mBroadcastDispatcher = broadcastDispatcher;
|
||||
@@ -181,6 +191,7 @@ public class PhoneStatusBarPolicy
|
||||
mProvisionedController = deviceProvisionedController;
|
||||
mKeyguardStateController = keyguardStateController;
|
||||
mLocationController = locationController;
|
||||
mPrivacyItemController = privacyItemController;
|
||||
mSensorPrivacyController = sensorPrivacyController;
|
||||
mRecordingController = recordingController;
|
||||
mUiBgExecutor = uiBgExecutor;
|
||||
@@ -200,6 +211,8 @@ public class PhoneStatusBarPolicy
|
||||
mSlotHeadset = resources.getString(com.android.internal.R.string.status_bar_headset);
|
||||
mSlotDataSaver = resources.getString(com.android.internal.R.string.status_bar_data_saver);
|
||||
mSlotLocation = resources.getString(com.android.internal.R.string.status_bar_location);
|
||||
mSlotMicrophone = resources.getString(com.android.internal.R.string.status_bar_microphone);
|
||||
mSlotCamera = resources.getString(com.android.internal.R.string.status_bar_camera);
|
||||
mSlotSensorsOff = resources.getString(com.android.internal.R.string.status_bar_sensors_off);
|
||||
mSlotScreenRecord = resources.getString(
|
||||
com.android.internal.R.string.status_bar_screen_record);
|
||||
@@ -271,6 +284,13 @@ public class PhoneStatusBarPolicy
|
||||
mResources.getString(R.string.accessibility_data_saver_on));
|
||||
mIconController.setIconVisibility(mSlotDataSaver, false);
|
||||
|
||||
// privacy items
|
||||
mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(),
|
||||
mResources.getString(PrivacyType.TYPE_MICROPHONE.getNameId()));
|
||||
mIconController.setIconVisibility(mSlotMicrophone, false);
|
||||
mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(),
|
||||
mResources.getString(PrivacyType.TYPE_CAMERA.getNameId()));
|
||||
mIconController.setIconVisibility(mSlotCamera, false);
|
||||
mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
|
||||
mResources.getString(R.string.accessibility_location_active));
|
||||
mIconController.setIconVisibility(mSlotLocation, false);
|
||||
@@ -294,6 +314,7 @@ public class PhoneStatusBarPolicy
|
||||
mNextAlarmController.addCallback(mNextAlarmCallback);
|
||||
mDataSaver.addCallback(this);
|
||||
mKeyguardStateController.addCallback(this);
|
||||
mPrivacyItemController.addCallback(this);
|
||||
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
|
||||
mLocationController.addCallback(this);
|
||||
mRecordingController.addCallback(this);
|
||||
@@ -609,9 +630,46 @@ public class PhoneStatusBarPolicy
|
||||
mIconController.setIconVisibility(mSlotDataSaver, isDataSaving);
|
||||
}
|
||||
|
||||
@Override // PrivacyItemController.Callback
|
||||
public void privacyChanged(List<PrivacyItem> privacyItems) {
|
||||
updatePrivacyItems(privacyItems);
|
||||
}
|
||||
|
||||
private void updatePrivacyItems(List<PrivacyItem> items) {
|
||||
boolean showCamera = false;
|
||||
boolean showMicrophone = false;
|
||||
boolean showLocation = false;
|
||||
for (PrivacyItem item : items) {
|
||||
if (item == null /* b/124234367 */) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "updatePrivacyItems - null item found");
|
||||
StringWriter out = new StringWriter();
|
||||
mPrivacyItemController.dump(null, new PrintWriter(out), null);
|
||||
Log.e(TAG, out.toString());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
switch (item.getPrivacyType()) {
|
||||
case TYPE_CAMERA:
|
||||
showCamera = true;
|
||||
break;
|
||||
case TYPE_LOCATION:
|
||||
showLocation = true;
|
||||
break;
|
||||
case TYPE_MICROPHONE:
|
||||
showMicrophone = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mIconController.setIconVisibility(mSlotCamera, showCamera);
|
||||
mIconController.setIconVisibility(mSlotMicrophone, showMicrophone);
|
||||
mIconController.setIconVisibility(mSlotLocation, showLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationActiveChanged(boolean active) {
|
||||
updateLocation();
|
||||
if (!mPrivacyItemController.isPermissionsHubEnabled()) updateLocation();
|
||||
}
|
||||
|
||||
// Updates the status view based on the current state of location requests.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 androidx.test.filters.SmallTest
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class PrivacyChipBuilderTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
val TEST_UID = 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateAppsList() {
|
||||
val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
|
||||
"Bar", TEST_UID))
|
||||
val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
|
||||
"Bar", TEST_UID))
|
||||
val foo0 = PrivacyItem(Privacy.TYPE_MICROPHONE, PrivacyApplication(
|
||||
"Foo", TEST_UID))
|
||||
val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
|
||||
"Baz", TEST_UID))
|
||||
|
||||
val items = listOf(bar2, foo0, baz1, bar3)
|
||||
|
||||
val textBuilder = PrivacyChipBuilder(context, items)
|
||||
|
||||
val list = textBuilder.appsAndTypes
|
||||
assertEquals(3, list.size)
|
||||
val appsList = list.map { it.first }
|
||||
val typesList = list.map { it.second }
|
||||
// List is sorted by number of types and then by types
|
||||
assertEquals(listOf("Bar", "Baz", "Foo"), appsList.map { it.packageName })
|
||||
assertEquals(listOf(Privacy.TYPE_CAMERA, Privacy.TYPE_LOCATION), typesList[0])
|
||||
assertEquals(listOf(Privacy.TYPE_CAMERA), typesList[1])
|
||||
assertEquals(listOf(Privacy.TYPE_MICROPHONE), typesList[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOrder() {
|
||||
// We want location to always go last, so it will go in the "+ other apps"
|
||||
val appCamera = PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||
PrivacyApplication("Camera", TEST_UID))
|
||||
val appMicrophone =
|
||||
PrivacyItem(PrivacyType.TYPE_MICROPHONE,
|
||||
PrivacyApplication("Microphone", TEST_UID))
|
||||
val appLocation =
|
||||
PrivacyItem(PrivacyType.TYPE_LOCATION,
|
||||
PrivacyApplication("Location", TEST_UID))
|
||||
|
||||
val items = listOf(appLocation, appMicrophone, appCamera)
|
||||
val textBuilder = PrivacyChipBuilder(context, items)
|
||||
val appList = textBuilder.appsAndTypes.map { it.first }.map { it.packageName }
|
||||
assertEquals(listOf("Camera", "Microphone", "Location"), appList)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.app.AppOpsManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.UserInfo
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.provider.DeviceConfig
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.appops.AppOpItem
|
||||
import com.android.systemui.appops.AppOpsController
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.util.DeviceConfigProxy
|
||||
import com.android.systemui.util.DeviceConfigProxyFake
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import org.hamcrest.Matchers.hasItem
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertThat
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.ArgumentMatchers.anyList
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.atLeastOnce
|
||||
import org.mockito.Mockito.doReturn
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@SmallTest
|
||||
@RunWithLooper
|
||||
class PrivacyItemControllerTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
val CURRENT_USER_ID = ActivityManager.getCurrentUser()
|
||||
val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
|
||||
const val SYSTEM_UID = 1000
|
||||
const val TEST_PACKAGE_NAME = "test"
|
||||
const val DEVICE_SERVICES_STRING = "Device services"
|
||||
const val TAG = "PrivacyItemControllerTest"
|
||||
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
|
||||
fun <T> eq(value: T): T = Mockito.eq(value) ?: value
|
||||
fun <T> any(): T = Mockito.any<T>()
|
||||
}
|
||||
|
||||
@Mock
|
||||
private lateinit var appOpsController: AppOpsController
|
||||
@Mock
|
||||
private lateinit var callback: PrivacyItemController.Callback
|
||||
@Mock
|
||||
private lateinit var userManager: UserManager
|
||||
@Mock
|
||||
private lateinit var broadcastDispatcher: BroadcastDispatcher
|
||||
@Mock
|
||||
private lateinit var dumpManager: DumpManager
|
||||
@Captor
|
||||
private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
|
||||
@Captor
|
||||
private lateinit var argCaptorCallback: ArgumentCaptor<AppOpsController.Callback>
|
||||
|
||||
private lateinit var privacyItemController: PrivacyItemController
|
||||
private lateinit var executor: FakeExecutor
|
||||
private lateinit var deviceConfigProxy: DeviceConfigProxy
|
||||
|
||||
fun PrivacyItemController(context: Context): PrivacyItemController {
|
||||
return PrivacyItemController(
|
||||
context,
|
||||
appOpsController,
|
||||
executor,
|
||||
executor,
|
||||
broadcastDispatcher,
|
||||
deviceConfigProxy,
|
||||
dumpManager
|
||||
)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
executor = FakeExecutor(FakeSystemClock())
|
||||
deviceConfigProxy = DeviceConfigProxyFake()
|
||||
|
||||
appOpsController = mDependency.injectMockDependency(AppOpsController::class.java)
|
||||
mContext.addMockSystemService(UserManager::class.java, userManager)
|
||||
|
||||
deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
|
||||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
|
||||
"true", false)
|
||||
|
||||
doReturn(listOf(object : UserInfo() {
|
||||
init {
|
||||
id = CURRENT_USER_ID
|
||||
}
|
||||
})).`when`(userManager).getProfiles(anyInt())
|
||||
|
||||
privacyItemController = PrivacyItemController(mContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetListeningTrueByAddingCallback() {
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
|
||||
any())
|
||||
verify(callback).privacyChanged(anyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetListeningFalseByRemovingLastCallback() {
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(appOpsController, never()).removeCallback(any(),
|
||||
any())
|
||||
privacyItemController.removeCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
|
||||
any())
|
||||
verify(callback).privacyChanged(emptyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDistinctItems() {
|
||||
doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
|
||||
AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1)))
|
||||
.`when`(appOpsController).getActiveAppOpsForUser(anyInt())
|
||||
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(callback).privacyChanged(capture(argCaptor))
|
||||
assertEquals(1, argCaptor.value.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRegisterReceiver_allUsers() {
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(broadcastDispatcher, atLeastOnce()).registerReceiver(
|
||||
eq(privacyItemController.userSwitcherReceiver), any(), eq(null), eq(UserHandle.ALL))
|
||||
verify(broadcastDispatcher, never())
|
||||
.unregisterReceiver(eq(privacyItemController.userSwitcherReceiver))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReceiver_ACTION_USER_FOREGROUND() {
|
||||
privacyItemController.userSwitcherReceiver.onReceive(context,
|
||||
Intent(Intent.ACTION_USER_SWITCHED))
|
||||
executor.runAllReady()
|
||||
verify(userManager).getProfiles(anyInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReceiver_ACTION_MANAGED_PROFILE_ADDED() {
|
||||
privacyItemController.userSwitcherReceiver.onReceive(context,
|
||||
Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE))
|
||||
executor.runAllReady()
|
||||
verify(userManager).getProfiles(anyInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReceiver_ACTION_MANAGED_PROFILE_REMOVED() {
|
||||
privacyItemController.userSwitcherReceiver.onReceive(context,
|
||||
Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE))
|
||||
executor.runAllReady()
|
||||
verify(userManager).getProfiles(anyInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddMultipleCallbacks() {
|
||||
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(callback).privacyChanged(anyList())
|
||||
|
||||
privacyItemController.addCallback(otherCallback)
|
||||
executor.runAllReady()
|
||||
verify(otherCallback).privacyChanged(anyList())
|
||||
// Adding a callback should not unnecessarily call previous ones
|
||||
verifyNoMoreInteractions(callback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleCallbacksAreUpdated() {
|
||||
doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOpsForUser(anyInt())
|
||||
|
||||
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||
privacyItemController.addCallback(callback)
|
||||
privacyItemController.addCallback(otherCallback)
|
||||
executor.runAllReady()
|
||||
reset(callback)
|
||||
reset(otherCallback)
|
||||
|
||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
||||
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
|
||||
executor.runAllReady()
|
||||
verify(callback).privacyChanged(anyList())
|
||||
verify(otherCallback).privacyChanged(anyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveCallback() {
|
||||
doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOpsForUser(anyInt())
|
||||
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||
privacyItemController.addCallback(callback)
|
||||
privacyItemController.addCallback(otherCallback)
|
||||
executor.runAllReady()
|
||||
executor.runAllReady()
|
||||
reset(callback)
|
||||
reset(otherCallback)
|
||||
|
||||
verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
|
||||
privacyItemController.removeCallback(callback)
|
||||
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
|
||||
executor.runAllReady()
|
||||
verify(callback, never()).privacyChanged(anyList())
|
||||
verify(otherCallback).privacyChanged(anyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListShouldNotHaveNull() {
|
||||
doReturn(listOf(AppOpItem(AppOpsManager.OP_ACTIVATE_VPN, TEST_UID, "", 0),
|
||||
AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, "", 0)))
|
||||
.`when`(appOpsController).getActiveAppOpsForUser(anyInt())
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
executor.runAllReady()
|
||||
|
||||
verify(callback).privacyChanged(capture(argCaptor))
|
||||
assertEquals(1, argCaptor.value.size)
|
||||
assertThat(argCaptor.value, not(hasItem(nullValue())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListShouldBeCopy() {
|
||||
val list = listOf(PrivacyItem(PrivacyType.TYPE_CAMERA,
|
||||
PrivacyApplication("", TEST_UID)))
|
||||
privacyItemController.privacyList = list
|
||||
val privacyList = privacyItemController.privacyList
|
||||
assertEquals(list, privacyList)
|
||||
assertTrue(list !== privacyList)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNotListeningWhenIndicatorsDisabled() {
|
||||
deviceConfigProxy.setProperty(
|
||||
DeviceConfig.NAMESPACE_PRIVACY,
|
||||
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
|
||||
"false",
|
||||
false
|
||||
)
|
||||
privacyItemController.addCallback(callback)
|
||||
executor.runAllReady()
|
||||
verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
|
||||
any())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user