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";
|
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
|
// Flags related to Assistant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -81,6 +81,21 @@
|
|||||||
<dimen name="car_keyline_2">96dp</dimen>
|
<dimen name="car_keyline_2">96dp</dimen>
|
||||||
<dimen name="car_keyline_3">128dp</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. -->
|
<!-- Car volume dimens. -->
|
||||||
<dimen name="car_volume_item_icon_size">@dimen/car_primary_icon_size</dimen>
|
<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>
|
<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
|
** See the License for the specific language governing permissions and
|
||||||
** limitations under the License.
|
** limitations under the License.
|
||||||
-->
|
-->
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:systemui="http://schemas.android.com/apk/res-auto"
|
xmlns:systemui="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/quick_status_bar_system_icons"
|
android:id="@+id/quick_status_bar_system_icons"
|
||||||
@@ -27,6 +27,13 @@
|
|||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:paddingTop="@dimen/status_bar_padding_top" >
|
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
|
<com.android.systemui.statusbar.policy.Clock
|
||||||
android:id="@+id/clock"
|
android:id="@+id/clock"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -38,5 +45,23 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
|
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
|
||||||
systemui:showDark="false" />
|
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>
|
<item>com.android.systemui</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<integer name="ongoing_appops_dialog_max_apps">5</integer>
|
||||||
|
|
||||||
<!-- Launcher package name for overlaying icons. -->
|
<!-- Launcher package name for overlaying icons. -->
|
||||||
<string name="launcher_overlayable_package" translatable="false">com.android.launcher3</string>
|
<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 -->
|
<!-- How much into a DisplayCutout's bounds we can go, on each side -->
|
||||||
<dimen name="display_cutout_margin_consumption">0px</dimen>
|
<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. -->
|
<!-- How much each bubble is elevated. -->
|
||||||
<dimen name="bubble_elevation">1dp</dimen>
|
<dimen name="bubble_elevation">1dp</dimen>
|
||||||
<!-- How much the bubble flyout text container is elevated. -->
|
<!-- 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] -->
|
app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
|
||||||
<string name="heap_dump_tile_name">Dump SysUI Heap</string>
|
<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] -->
|
<!-- Text for the quick setting tile for sensor privacy [CHAR LIMIT=30] -->
|
||||||
<string name="sensor_privacy_mode">Sensors off</string>
|
<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.plugins.statusbar.StatusBarStateController;
|
||||||
import com.android.systemui.power.EnhancedEstimates;
|
import com.android.systemui.power.EnhancedEstimates;
|
||||||
import com.android.systemui.power.PowerUI;
|
import com.android.systemui.power.PowerUI;
|
||||||
|
import com.android.systemui.privacy.PrivacyItemController;
|
||||||
import com.android.systemui.recents.OverviewProxyService;
|
import com.android.systemui.recents.OverviewProxyService;
|
||||||
import com.android.systemui.recents.Recents;
|
import com.android.systemui.recents.Recents;
|
||||||
import com.android.systemui.screenrecord.RecordingController;
|
import com.android.systemui.screenrecord.RecordingController;
|
||||||
@@ -294,6 +295,7 @@ public class Dependency {
|
|||||||
@Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager;
|
@Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager;
|
||||||
@Inject Lazy<AutoHideController> mAutoHideController;
|
@Inject Lazy<AutoHideController> mAutoHideController;
|
||||||
@Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener;
|
@Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener;
|
||||||
|
@Inject Lazy<PrivacyItemController> mPrivacyItemController;
|
||||||
@Inject @Background Lazy<Looper> mBgLooper;
|
@Inject @Background Lazy<Looper> mBgLooper;
|
||||||
@Inject @Background Lazy<Handler> mBgHandler;
|
@Inject @Background Lazy<Handler> mBgHandler;
|
||||||
@Inject @Main Lazy<Looper> mMainLooper;
|
@Inject @Main Lazy<Looper> mMainLooper;
|
||||||
@@ -491,6 +493,7 @@ public class Dependency {
|
|||||||
mProviders.put(ForegroundServiceNotificationListener.class,
|
mProviders.put(ForegroundServiceNotificationListener.class,
|
||||||
mForegroundServiceNotificationListener::get);
|
mForegroundServiceNotificationListener::get);
|
||||||
mProviders.put(ClockManager.class, mClockManager::get);
|
mProviders.put(ClockManager.class, mClockManager::get);
|
||||||
|
mProviders.put(PrivacyItemController.class, mPrivacyItemController::get);
|
||||||
mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get);
|
mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get);
|
||||||
mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get);
|
mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get);
|
||||||
mProviders.put(PackageManagerWrapper.class, mPackageManagerWrapper::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 long NOTED_OP_TIME_DELAY_MS = 5000;
|
||||||
private static final String TAG = "AppOpsControllerImpl";
|
private static final String TAG = "AppOpsControllerImpl";
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
private final Context mContext;
|
|
||||||
|
|
||||||
private final AppOpsManager mAppOps;
|
private final AppOpsManager mAppOps;
|
||||||
private H mBGHandler;
|
private H mBGHandler;
|
||||||
@@ -83,7 +82,6 @@ public class AppOpsControllerImpl implements AppOpsController,
|
|||||||
Context context,
|
Context context,
|
||||||
@Background Looper bgLooper,
|
@Background Looper bgLooper,
|
||||||
DumpManager dumpManager) {
|
DumpManager dumpManager) {
|
||||||
mContext = context;
|
|
||||||
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
|
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
|
||||||
mBGHandler = new H(bgLooper);
|
mBGHandler = new H(bgLooper);
|
||||||
final int numOps = OPS.length;
|
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.graphics.Rect;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.provider.AlarmClock;
|
import android.provider.AlarmClock;
|
||||||
|
import android.provider.DeviceConfig;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.service.notification.ZenModeConfig;
|
import android.service.notification.ZenModeConfig;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
@@ -46,7 +48,9 @@ import android.view.ViewGroup;
|
|||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
|
import android.widget.Space;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -55,6 +59,8 @@ import androidx.lifecycle.Lifecycle;
|
|||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LifecycleRegistry;
|
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.settingslib.Utils;
|
||||||
import com.android.systemui.BatteryMeterView;
|
import com.android.systemui.BatteryMeterView;
|
||||||
import com.android.systemui.DualToneHandler;
|
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.ActivityStarter;
|
||||||
import com.android.systemui.plugins.DarkIconDispatcher;
|
import com.android.systemui.plugins.DarkIconDispatcher;
|
||||||
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
|
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.QSDetail.Callback;
|
||||||
import com.android.systemui.qs.carrier.QSCarrierGroup;
|
import com.android.systemui.qs.carrier.QSCarrierGroup;
|
||||||
import com.android.systemui.statusbar.CommandQueue;
|
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;
|
private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0;
|
||||||
public static final int MAX_TOOLTIP_SHOWN_COUNT = 2;
|
public static final int MAX_TOOLTIP_SHOWN_COUNT = 2;
|
||||||
|
|
||||||
private final Handler mHandler = new Handler();
|
|
||||||
private final NextAlarmController mAlarmController;
|
private final NextAlarmController mAlarmController;
|
||||||
private final ZenModeController mZenController;
|
private final ZenModeController mZenController;
|
||||||
private final StatusBarIconController mStatusBarIconController;
|
private final StatusBarIconController mStatusBarIconController;
|
||||||
@@ -140,9 +150,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
private View mRingerContainer;
|
private View mRingerContainer;
|
||||||
private Clock mClockView;
|
private Clock mClockView;
|
||||||
private DateView mDateView;
|
private DateView mDateView;
|
||||||
|
private OngoingPrivacyChip mPrivacyChip;
|
||||||
|
private Space mSpace;
|
||||||
private BatteryMeterView mBatteryRemainingIcon;
|
private BatteryMeterView mBatteryRemainingIcon;
|
||||||
private RingerModeTracker mRingerModeTracker;
|
private RingerModeTracker mRingerModeTracker;
|
||||||
|
private boolean mPermissionsHubEnabled;
|
||||||
|
|
||||||
|
private PrivacyItemController mPrivacyItemController;
|
||||||
|
private final UiEventLogger mUiEventLogger;
|
||||||
// Used for RingerModeTracker
|
// Used for RingerModeTracker
|
||||||
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
|
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
|
||||||
|
|
||||||
@@ -156,22 +171,49 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
private int mCutOutPaddingRight;
|
private int mCutOutPaddingRight;
|
||||||
private float mExpandedHeaderAlpha = 1.0f;
|
private float mExpandedHeaderAlpha = 1.0f;
|
||||||
private float mKeyguardExpansionFraction;
|
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
|
@Inject
|
||||||
public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
|
public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
|
||||||
NextAlarmController nextAlarmController, ZenModeController zenModeController,
|
NextAlarmController nextAlarmController, ZenModeController zenModeController,
|
||||||
StatusBarIconController statusBarIconController,
|
StatusBarIconController statusBarIconController,
|
||||||
ActivityStarter activityStarter,
|
ActivityStarter activityStarter, PrivacyItemController privacyItemController,
|
||||||
CommandQueue commandQueue, RingerModeTracker ringerModeTracker) {
|
CommandQueue commandQueue, RingerModeTracker ringerModeTracker,
|
||||||
|
UiEventLogger uiEventLogger) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
mAlarmController = nextAlarmController;
|
mAlarmController = nextAlarmController;
|
||||||
mZenController = zenModeController;
|
mZenController = zenModeController;
|
||||||
mStatusBarIconController = statusBarIconController;
|
mStatusBarIconController = statusBarIconController;
|
||||||
mActivityStarter = activityStarter;
|
mActivityStarter = activityStarter;
|
||||||
|
mPrivacyItemController = privacyItemController;
|
||||||
mDualToneHandler = new DualToneHandler(
|
mDualToneHandler = new DualToneHandler(
|
||||||
new ContextThemeWrapper(context, R.style.QSHeaderTheme));
|
new ContextThemeWrapper(context, R.style.QSHeaderTheme));
|
||||||
mCommandQueue = commandQueue;
|
mCommandQueue = commandQueue;
|
||||||
mRingerModeTracker = ringerModeTracker;
|
mRingerModeTracker = ringerModeTracker;
|
||||||
|
mUiEventLogger = uiEventLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -198,8 +240,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mRingerModeTextView = findViewById(R.id.ringer_mode_text);
|
mRingerModeTextView = findViewById(R.id.ringer_mode_text);
|
||||||
mRingerContainer = findViewById(R.id.ringer_container);
|
mRingerContainer = findViewById(R.id.ringer_container);
|
||||||
mRingerContainer.setOnClickListener(this::onClick);
|
mRingerContainer.setOnClickListener(this::onClick);
|
||||||
|
mPrivacyChip = findViewById(R.id.privacy_chip);
|
||||||
|
mPrivacyChip.setOnClickListener(this::onClick);
|
||||||
mCarrierGroup = findViewById(R.id.carrier_group);
|
mCarrierGroup = findViewById(R.id.carrier_group);
|
||||||
|
|
||||||
|
|
||||||
updateResources();
|
updateResources();
|
||||||
|
|
||||||
Rect tintArea = new Rect(0, 0, 0, 0);
|
Rect tintArea = new Rect(0, 0, 0, 0);
|
||||||
@@ -219,6 +264,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mClockView = findViewById(R.id.clock);
|
mClockView = findViewById(R.id.clock);
|
||||||
mClockView.setOnClickListener(this);
|
mClockView.setOnClickListener(this);
|
||||||
mDateView = findViewById(R.id.date);
|
mDateView = findViewById(R.id.date);
|
||||||
|
mSpace = findViewById(R.id.space);
|
||||||
|
|
||||||
// Tint for the battery icons are handled in setupHost()
|
// Tint for the battery icons are handled in setupHost()
|
||||||
mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon);
|
mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon);
|
||||||
@@ -229,6 +275,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
|
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
|
||||||
mRingerModeTextView.setSelected(true);
|
mRingerModeTextView.setSelected(true);
|
||||||
mNextAlarmTextView.setSelected(true);
|
mNextAlarmTextView.setSelected(true);
|
||||||
|
|
||||||
|
mPermissionsHubEnabled = mPrivacyItemController.isPermissionsHubEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public QuickQSPanel getHeaderQsPanel() {
|
public QuickQSPanel getHeaderQsPanel() {
|
||||||
@@ -241,6 +289,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
com.android.internal.R.string.status_bar_camera));
|
com.android.internal.R.string.status_bar_camera));
|
||||||
ignored.add(mContext.getResources().getString(
|
ignored.add(mContext.getResources().getString(
|
||||||
com.android.internal.R.string.status_bar_microphone));
|
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;
|
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() {
|
private boolean updateRingerStatus() {
|
||||||
boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
|
boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
|
||||||
CharSequence originalRingerText = mRingerModeTextView.getText();
|
CharSequence originalRingerText = mRingerModeTextView.getText();
|
||||||
@@ -363,6 +429,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
|
|
||||||
updateStatusIconAlphaAnimator();
|
updateStatusIconAlphaAnimator();
|
||||||
updateHeaderTextContainerAlphaAnimator();
|
updateHeaderTextContainerAlphaAnimator();
|
||||||
|
updatePrivacyChipAlphaAnimator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatusIconAlphaAnimator() {
|
private void updateStatusIconAlphaAnimator() {
|
||||||
@@ -377,6 +444,12 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updatePrivacyChipAlphaAnimator() {
|
||||||
|
mPrivacyChipAlphaAnimator = new TouchAnimator.Builder()
|
||||||
|
.addFloat(mPrivacyChip, "alpha", 1, 0, 1)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public void setExpanded(boolean expanded) {
|
public void setExpanded(boolean expanded) {
|
||||||
if (mExpanded == expanded) return;
|
if (mExpanded == expanded) return;
|
||||||
mExpanded = expanded;
|
mExpanded = expanded;
|
||||||
@@ -415,6 +488,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mHeaderTextContainerView.setVisibility(INVISIBLE);
|
mHeaderTextContainerView.setVisibility(INVISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mPrivacyChipAlphaAnimator != null) {
|
||||||
|
mPrivacyChip.setExpanded(expansionFraction > 0.5);
|
||||||
|
mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction);
|
||||||
|
}
|
||||||
if (expansionFraction < 1 && expansionFraction > 0.99) {
|
if (expansionFraction < 1 && expansionFraction > 0.99) {
|
||||||
if (mHeaderQsPanel.switchTileLayout()) {
|
if (mHeaderQsPanel.switchTileLayout()) {
|
||||||
updateResources();
|
updateResources();
|
||||||
@@ -442,6 +519,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
});
|
});
|
||||||
mStatusBarIconController.addIconGroup(mIconManager);
|
mStatusBarIconController.addIconGroup(mIconManager);
|
||||||
requestApplyInsets();
|
requestApplyInsets();
|
||||||
|
// Change the ignored slots when DeviceConfig flag changes
|
||||||
|
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
|
||||||
|
mContext.getMainExecutor(), mPropertiesListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -453,6 +533,31 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
Pair<Integer, Integer> padding =
|
Pair<Integer, Integer> padding =
|
||||||
StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner(
|
StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner(
|
||||||
cutout, cornerCutoutPadding, -1);
|
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;
|
mCutOutPaddingLeft = padding.first;
|
||||||
mCutOutPaddingRight = padding.second;
|
mCutOutPaddingRight = padding.second;
|
||||||
mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top;
|
mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top;
|
||||||
@@ -496,6 +601,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
setListening(false);
|
setListening(false);
|
||||||
mRingerModeTracker.getRingerModeInternal().removeObservers(this);
|
mRingerModeTracker.getRingerModeInternal().removeObservers(this);
|
||||||
mStatusBarIconController.removeIconGroup(mIconManager);
|
mStatusBarIconController.removeIconGroup(mIconManager);
|
||||||
|
DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener);
|
||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,10 +619,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mZenController.addCallback(this);
|
mZenController.addCallback(this);
|
||||||
mAlarmController.addCallback(this);
|
mAlarmController.addCallback(this);
|
||||||
mLifecycle.setCurrentState(Lifecycle.State.RESUMED);
|
mLifecycle.setCurrentState(Lifecycle.State.RESUMED);
|
||||||
|
mPrivacyItemController.addCallback(mPICCallback);
|
||||||
} else {
|
} else {
|
||||||
mZenController.removeCallback(this);
|
mZenController.removeCallback(this);
|
||||||
mAlarmController.removeCallback(this);
|
mAlarmController.removeCallback(this);
|
||||||
mLifecycle.setCurrentState(Lifecycle.State.CREATED);
|
mLifecycle.setCurrentState(Lifecycle.State.CREATED);
|
||||||
|
mPrivacyItemController.removeCallback(mPICCallback);
|
||||||
|
mPrivacyChipLogged = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,6 +643,17 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
|
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
|
||||||
AlarmClock.ACTION_SHOW_ALARMS), 0);
|
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()) {
|
} else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) {
|
||||||
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
|
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
|
||||||
Settings.ACTION_SOUND_SETTINGS), 0);
|
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.DisplayId;
|
||||||
import com.android.systemui.dagger.qualifiers.Main;
|
import com.android.systemui.dagger.qualifiers.Main;
|
||||||
import com.android.systemui.dagger.qualifiers.UiBackground;
|
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.DndTile;
|
||||||
import com.android.systemui.qs.tiles.RotationLockTile;
|
import com.android.systemui.qs.tiles.RotationLockTile;
|
||||||
import com.android.systemui.screenrecord.RecordingController;
|
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.RingerModeTracker;
|
||||||
import com.android.systemui.util.time.DateFormatUtil;
|
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.Locale;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
@@ -87,13 +93,13 @@ public class PhoneStatusBarPolicy
|
|||||||
ZenModeController.Callback,
|
ZenModeController.Callback,
|
||||||
DeviceProvisionedListener,
|
DeviceProvisionedListener,
|
||||||
KeyguardStateController.Callback,
|
KeyguardStateController.Callback,
|
||||||
|
PrivacyItemController.Callback,
|
||||||
LocationController.LocationChangeCallback,
|
LocationController.LocationChangeCallback,
|
||||||
RecordingController.RecordingStateChangeCallback {
|
RecordingController.RecordingStateChangeCallback {
|
||||||
private static final String TAG = "PhoneStatusBarPolicy";
|
private static final String TAG = "PhoneStatusBarPolicy";
|
||||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||||
|
|
||||||
static final int LOCATION_STATUS_ICON_ID =
|
static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId();
|
||||||
com.android.internal.R.drawable.perm_group_location;
|
|
||||||
|
|
||||||
private final String mSlotCast;
|
private final String mSlotCast;
|
||||||
private final String mSlotHotspot;
|
private final String mSlotHotspot;
|
||||||
@@ -107,6 +113,8 @@ public class PhoneStatusBarPolicy
|
|||||||
private final String mSlotHeadset;
|
private final String mSlotHeadset;
|
||||||
private final String mSlotDataSaver;
|
private final String mSlotDataSaver;
|
||||||
private final String mSlotLocation;
|
private final String mSlotLocation;
|
||||||
|
private final String mSlotMicrophone;
|
||||||
|
private final String mSlotCamera;
|
||||||
private final String mSlotSensorsOff;
|
private final String mSlotSensorsOff;
|
||||||
private final String mSlotScreenRecord;
|
private final String mSlotScreenRecord;
|
||||||
private final int mDisplayId;
|
private final int mDisplayId;
|
||||||
@@ -132,6 +140,7 @@ public class PhoneStatusBarPolicy
|
|||||||
private final DeviceProvisionedController mProvisionedController;
|
private final DeviceProvisionedController mProvisionedController;
|
||||||
private final KeyguardStateController mKeyguardStateController;
|
private final KeyguardStateController mKeyguardStateController;
|
||||||
private final LocationController mLocationController;
|
private final LocationController mLocationController;
|
||||||
|
private final PrivacyItemController mPrivacyItemController;
|
||||||
private final Executor mUiBgExecutor;
|
private final Executor mUiBgExecutor;
|
||||||
private final SensorPrivacyController mSensorPrivacyController;
|
private final SensorPrivacyController mSensorPrivacyController;
|
||||||
private final RecordingController mRecordingController;
|
private final RecordingController mRecordingController;
|
||||||
@@ -162,7 +171,8 @@ public class PhoneStatusBarPolicy
|
|||||||
RecordingController recordingController,
|
RecordingController recordingController,
|
||||||
@Nullable TelecomManager telecomManager, @DisplayId int displayId,
|
@Nullable TelecomManager telecomManager, @DisplayId int displayId,
|
||||||
@Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
|
@Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
|
||||||
RingerModeTracker ringerModeTracker) {
|
RingerModeTracker ringerModeTracker,
|
||||||
|
PrivacyItemController privacyItemController) {
|
||||||
mIconController = iconController;
|
mIconController = iconController;
|
||||||
mCommandQueue = commandQueue;
|
mCommandQueue = commandQueue;
|
||||||
mBroadcastDispatcher = broadcastDispatcher;
|
mBroadcastDispatcher = broadcastDispatcher;
|
||||||
@@ -181,6 +191,7 @@ public class PhoneStatusBarPolicy
|
|||||||
mProvisionedController = deviceProvisionedController;
|
mProvisionedController = deviceProvisionedController;
|
||||||
mKeyguardStateController = keyguardStateController;
|
mKeyguardStateController = keyguardStateController;
|
||||||
mLocationController = locationController;
|
mLocationController = locationController;
|
||||||
|
mPrivacyItemController = privacyItemController;
|
||||||
mSensorPrivacyController = sensorPrivacyController;
|
mSensorPrivacyController = sensorPrivacyController;
|
||||||
mRecordingController = recordingController;
|
mRecordingController = recordingController;
|
||||||
mUiBgExecutor = uiBgExecutor;
|
mUiBgExecutor = uiBgExecutor;
|
||||||
@@ -200,6 +211,8 @@ public class PhoneStatusBarPolicy
|
|||||||
mSlotHeadset = resources.getString(com.android.internal.R.string.status_bar_headset);
|
mSlotHeadset = resources.getString(com.android.internal.R.string.status_bar_headset);
|
||||||
mSlotDataSaver = resources.getString(com.android.internal.R.string.status_bar_data_saver);
|
mSlotDataSaver = resources.getString(com.android.internal.R.string.status_bar_data_saver);
|
||||||
mSlotLocation = resources.getString(com.android.internal.R.string.status_bar_location);
|
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);
|
mSlotSensorsOff = resources.getString(com.android.internal.R.string.status_bar_sensors_off);
|
||||||
mSlotScreenRecord = resources.getString(
|
mSlotScreenRecord = resources.getString(
|
||||||
com.android.internal.R.string.status_bar_screen_record);
|
com.android.internal.R.string.status_bar_screen_record);
|
||||||
@@ -271,6 +284,13 @@ public class PhoneStatusBarPolicy
|
|||||||
mResources.getString(R.string.accessibility_data_saver_on));
|
mResources.getString(R.string.accessibility_data_saver_on));
|
||||||
mIconController.setIconVisibility(mSlotDataSaver, false);
|
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,
|
mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
|
||||||
mResources.getString(R.string.accessibility_location_active));
|
mResources.getString(R.string.accessibility_location_active));
|
||||||
mIconController.setIconVisibility(mSlotLocation, false);
|
mIconController.setIconVisibility(mSlotLocation, false);
|
||||||
@@ -294,6 +314,7 @@ public class PhoneStatusBarPolicy
|
|||||||
mNextAlarmController.addCallback(mNextAlarmCallback);
|
mNextAlarmController.addCallback(mNextAlarmCallback);
|
||||||
mDataSaver.addCallback(this);
|
mDataSaver.addCallback(this);
|
||||||
mKeyguardStateController.addCallback(this);
|
mKeyguardStateController.addCallback(this);
|
||||||
|
mPrivacyItemController.addCallback(this);
|
||||||
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
|
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
|
||||||
mLocationController.addCallback(this);
|
mLocationController.addCallback(this);
|
||||||
mRecordingController.addCallback(this);
|
mRecordingController.addCallback(this);
|
||||||
@@ -609,9 +630,46 @@ public class PhoneStatusBarPolicy
|
|||||||
mIconController.setIconVisibility(mSlotDataSaver, isDataSaving);
|
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
|
@Override
|
||||||
public void onLocationActiveChanged(boolean active) {
|
public void onLocationActiveChanged(boolean active) {
|
||||||
updateLocation();
|
if (!mPrivacyItemController.isPermissionsHubEnabled()) updateLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the status view based on the current state of location requests.
|
// 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