Merge "DO NOT MERGE Revert "DO NOT MERGE Remove Privacy Indicators"" into qt-r1-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
005993172f
@@ -106,6 +106,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_enabled";
|
||||||
|
|
||||||
// Flags related to Assistant Handles
|
// Flags related to Assistant Handles
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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) 2018 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>
|
||||||
41
packages/SystemUI/res/layout/ongoing_privacy_chip.xml
Normal file
41
packages/SystemUI/res/layout/ongoing_privacy_chip.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2018 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:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:focusable="true" >
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/background"
|
||||||
|
android:layout_height="@dimen/ongoing_appops_chip_height"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:layout_width="wrap_content" >
|
||||||
|
<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>
|
||||||
24
packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
Normal file
24
packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2018 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textDirection="locale"
|
||||||
|
android:textAppearance="@style/TextAppearance.QS.DetailItemPrimary"
|
||||||
|
/>
|
||||||
@@ -22,11 +22,19 @@
|
|||||||
android:layout_height="@*android:dimen/quick_qs_offset_height"
|
android:layout_height="@*android:dimen/quick_qs_offset_height"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:paddingStart="@dimen/status_bar_padding_start"
|
android:paddingStart="@dimen/status_bar_padding_start"
|
||||||
android:paddingEnd="@dimen/status_bar_padding_end" >
|
android:paddingEnd="@dimen/status_bar_padding_end" >
|
||||||
|
|
||||||
|
<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,4 +46,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>
|
||||||
|
|
||||||
|
<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>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -48,6 +48,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.shared.plugins.PluginManager;
|
import com.android.systemui.shared.plugins.PluginManager;
|
||||||
import com.android.systemui.shared.system.ActivityManagerWrapper;
|
import com.android.systemui.shared.system.ActivityManagerWrapper;
|
||||||
@@ -286,6 +287,7 @@ public class Dependency extends SystemUI {
|
|||||||
@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 @Named(BG_LOOPER_NAME) Lazy<Looper> mBgLooper;
|
@Inject @Named(BG_LOOPER_NAME) Lazy<Looper> mBgLooper;
|
||||||
@Inject @Named(BG_HANDLER_NAME) Lazy<Handler> mBgHandler;
|
@Inject @Named(BG_HANDLER_NAME) Lazy<Handler> mBgHandler;
|
||||||
@Inject @Named(MAIN_HANDLER_NAME) Lazy<Handler> mMainHandler;
|
@Inject @Named(MAIN_HANDLER_NAME) Lazy<Handler> mMainHandler;
|
||||||
@@ -470,6 +472,7 @@ public class Dependency extends SystemUI {
|
|||||||
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);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import static com.android.systemui.Dependency.BG_LOOPER_NAME;
|
|||||||
|
|
||||||
import android.app.AppOpsManager;
|
import android.app.AppOpsManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
@@ -209,6 +210,59 @@ public class AppOpsControllerImpl implements AppOpsController,
|
|||||||
mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS);
|
mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the app-op code refer to a user sensitive permission for the specified user id
|
||||||
|
* and package. Only user sensitive permission should be shown to the user by default.
|
||||||
|
*
|
||||||
|
* @param appOpCode The code of the app-op.
|
||||||
|
* @param uid The uid of the user.
|
||||||
|
* @param packageName The name of the package.
|
||||||
|
*
|
||||||
|
* @return {@code true} iff the app-op item is user sensitive
|
||||||
|
*/
|
||||||
|
private boolean isUserSensitive(int appOpCode, int uid, String packageName) {
|
||||||
|
String permission = AppOpsManager.opToPermission(appOpCode);
|
||||||
|
if (permission == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int permFlags = mContext.getPackageManager().getPermissionFlags(permission,
|
||||||
|
packageName, UserHandle.getUserHandleForUid(uid));
|
||||||
|
return (permFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the app-op item refer to an operation that should be shown to the user.
|
||||||
|
* Only specficic ops (like SYSTEM_ALERT_WINDOW) or ops that refer to user sensitive
|
||||||
|
* permission should be shown to the user by default.
|
||||||
|
*
|
||||||
|
* @param item The item
|
||||||
|
*
|
||||||
|
* @return {@code true} iff the app-op item should be shown to the user
|
||||||
|
*/
|
||||||
|
private boolean isUserVisible(AppOpItem item) {
|
||||||
|
return isUserVisible(item.getCode(), item.getUid(), item.getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the app-op, uid and package name, refer to an operation that should be shown to the
|
||||||
|
* user. Only specficic ops (like {@link AppOpsManager.OP_SYSTEM_ALERT_WINDOW}) or
|
||||||
|
* ops that refer to user sensitive permission should be shown to the user by default.
|
||||||
|
*
|
||||||
|
* @param item The item
|
||||||
|
*
|
||||||
|
* @return {@code true} iff the app-op for should be shown to the user
|
||||||
|
*/
|
||||||
|
private boolean isUserVisible(int appOpCode, int uid, String packageName) {
|
||||||
|
// currently OP_SYSTEM_ALERT_WINDOW does not correspond to a platform permission
|
||||||
|
// which may be user senstive, so for now always show it to the user.
|
||||||
|
if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isUserSensitive(appOpCode, uid, packageName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the list containing all the active AppOps that the controller tracks.
|
* Returns a copy of the list containing all the active AppOps that the controller tracks.
|
||||||
*
|
*
|
||||||
@@ -232,8 +286,8 @@ public class AppOpsControllerImpl implements AppOpsController,
|
|||||||
final int numActiveItems = mActiveItems.size();
|
final int numActiveItems = mActiveItems.size();
|
||||||
for (int i = 0; i < numActiveItems; i++) {
|
for (int i = 0; i < numActiveItems; i++) {
|
||||||
AppOpItem item = mActiveItems.get(i);
|
AppOpItem item = mActiveItems.get(i);
|
||||||
if ((userId == UserHandle.USER_ALL
|
if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)
|
||||||
|| UserHandle.getUserId(item.getUid()) == userId)) {
|
&& isUserVisible(item)) {
|
||||||
list.add(item);
|
list.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,8 +296,8 @@ public class AppOpsControllerImpl implements AppOpsController,
|
|||||||
final int numNotedItems = mNotedItems.size();
|
final int numNotedItems = mNotedItems.size();
|
||||||
for (int i = 0; i < numNotedItems; i++) {
|
for (int i = 0; i < numNotedItems; i++) {
|
||||||
AppOpItem item = mNotedItems.get(i);
|
AppOpItem item = mNotedItems.get(i);
|
||||||
if ((userId == UserHandle.USER_ALL
|
if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)
|
||||||
|| UserHandle.getUserId(item.getUid()) == userId)) {
|
&& isUserVisible(item)) {
|
||||||
list.add(item);
|
list.add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +323,8 @@ public class AppOpsControllerImpl implements AppOpsController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void notifySuscribers(int code, int uid, String packageName, boolean active) {
|
private void notifySuscribers(int code, int uid, String packageName, boolean active) {
|
||||||
if (mCallbacksByCode.containsKey(code)) {
|
if (mCallbacksByCode.containsKey(code)
|
||||||
|
&& isUserVisible(code, uid, packageName)) {
|
||||||
for (Callback cb: mCallbacksByCode.get(code)) {
|
for (Callback cb: mCallbacksByCode.get(code)) {
|
||||||
cb.onActiveStateChanged(code, uid, packageName, active);
|
cb.onActiveStateChanged(code, uid, packageName, active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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
|
||||||
|
) : LinearLayout(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 = PrivacyDialogBuilder(context, emptyList<PrivacyItem>())
|
||||||
|
var privacyList = emptyList<PrivacyItem>()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
builder = PrivacyDialogBuilder(context, value)
|
||||||
|
updateView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinishInflate() {
|
||||||
|
super.onFinishInflate()
|
||||||
|
|
||||||
|
back = findViewById(R.id.background)
|
||||||
|
iconsContainer = findViewById(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(dialogBuilder: PrivacyDialogBuilder, iconsContainer: ViewGroup) {
|
||||||
|
iconsContainer.removeAllViews()
|
||||||
|
dialogBuilder.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,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
|
||||||
|
* except in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the
|
||||||
|
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the specific language governing
|
||||||
|
* permissions and limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.android.systemui.R
|
||||||
|
|
||||||
|
class PrivacyDialogBuilder(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 generateIconsForApp(types: List<PrivacyType>): List<Drawable> {
|
||||||
|
return types.sorted().map { it.getIcon(context) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
|
||||||
|
* except in compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the
|
||||||
|
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the specific language governing
|
||||||
|
* permissions and limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.systemui.privacy
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.util.IconDrawableFactory
|
||||||
|
import com.android.systemui.R
|
||||||
|
|
||||||
|
typealias Privacy = PrivacyType
|
||||||
|
|
||||||
|
enum class PrivacyType(private 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, val context: Context)
|
||||||
|
: Comparable<PrivacyApplication> {
|
||||||
|
|
||||||
|
override fun compareTo(other: PrivacyApplication): Int {
|
||||||
|
return applicationName.compareTo(other.applicationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val applicationInfo: ApplicationInfo? by lazy {
|
||||||
|
try {
|
||||||
|
val userHandle = UserHandle.getUserHandleForUid(uid)
|
||||||
|
context.createPackageContextAsUser(packageName, 0, userHandle).getPackageManager()
|
||||||
|
.getApplicationInfo(packageName, 0)
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val icon: Drawable by lazy {
|
||||||
|
applicationInfo?.let {
|
||||||
|
try {
|
||||||
|
val iconFactory = IconDrawableFactory.newInstance(context, true)
|
||||||
|
iconFactory.getBadgedIcon(it, UserHandle.getUserId(uid))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: context.getDrawable(android.R.drawable.sym_def_app_icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
val applicationName: String by lazy {
|
||||||
|
applicationInfo?.let {
|
||||||
|
context.packageManager.getApplicationLabel(it) as String
|
||||||
|
} ?: packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString() = "PrivacyApplication(packageName=$packageName, uid=$uid)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Message
|
||||||
|
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.Dependency.BG_HANDLER_NAME
|
||||||
|
import com.android.systemui.Dependency.MAIN_HANDLER_NAME
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.appops.AppOpItem
|
||||||
|
import com.android.systemui.appops.AppOpsController
|
||||||
|
import com.android.systemui.Dumpable
|
||||||
|
import java.io.FileDescriptor
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
fun isPermissionsHubEnabled() = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
|
||||||
|
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class PrivacyItemController @Inject constructor(
|
||||||
|
val context: Context,
|
||||||
|
private val appOpsController: AppOpsController,
|
||||||
|
@Named(MAIN_HANDLER_NAME) private val uiHandler: Handler,
|
||||||
|
@Named(BG_HANDLER_NAME) private val bgHandler: Handler
|
||||||
|
) : Dumpable {
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal companion object {
|
||||||
|
val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
|
||||||
|
AppOpsManager.OP_RECORD_AUDIO,
|
||||||
|
AppOpsManager.OP_COARSE_LOCATION,
|
||||||
|
AppOpsManager.OP_FINE_LOCATION)
|
||||||
|
val intents = listOf(Intent.ACTION_USER_FOREGROUND,
|
||||||
|
Intent.ACTION_MANAGED_PROFILE_ADDED,
|
||||||
|
Intent.ACTION_MANAGED_PROFILE_REMOVED)
|
||||||
|
const val TAG = "PrivacyItemController"
|
||||||
|
const val SYSTEM_UID = 1000
|
||||||
|
const val MSG_ADD_CALLBACK = 0
|
||||||
|
const val MSG_REMOVE_CALLBACK = 1
|
||||||
|
const val MSG_UPDATE_LISTENING_STATE = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var privacyList = emptyList<PrivacyItem>()
|
||||||
|
@Synchronized get() = field.toList() // Returns a shallow copy of the list
|
||||||
|
@Synchronized set
|
||||||
|
|
||||||
|
private val userManager = context.getSystemService(UserManager::class.java)
|
||||||
|
private var currentUserIds = emptyList<Int>()
|
||||||
|
private var listening = false
|
||||||
|
val systemApp =
|
||||||
|
PrivacyApplication(context.getString(R.string.device_services), SYSTEM_UID, context)
|
||||||
|
private val callbacks = mutableListOf<WeakReference<Callback>>()
|
||||||
|
private val messageHandler = H(WeakReference(this), uiHandler.looper)
|
||||||
|
|
||||||
|
private val notifyChanges = Runnable {
|
||||||
|
val list = privacyList
|
||||||
|
callbacks.forEach { it.get()?.privacyChanged(list) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val updateListAndNotifyChanges = Runnable {
|
||||||
|
updatePrivacyList()
|
||||||
|
uiHandler.post(notifyChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var indicatorsAvailable = isPermissionsHubEnabled()
|
||||||
|
@VisibleForTesting
|
||||||
|
internal val devicePropertyChangedListener =
|
||||||
|
object : DeviceConfig.OnPropertyChangedListener {
|
||||||
|
override fun onPropertyChanged(namespace: String, name: String, value: String?) {
|
||||||
|
if (DeviceConfig.NAMESPACE_PRIVACY.equals(namespace) &&
|
||||||
|
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED.equals(name)) {
|
||||||
|
indicatorsAvailable = java.lang.Boolean.parseBoolean(value)
|
||||||
|
messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
|
||||||
|
messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
context.unregisterReceiver(field)
|
||||||
|
field = value
|
||||||
|
registerReceiver()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
DeviceConfig.addOnPropertyChangedListener(
|
||||||
|
DeviceConfig.NAMESPACE_PRIVACY, context.mainExecutor, devicePropertyChangedListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterReceiver() {
|
||||||
|
context.unregisterReceiver(userSwitcherReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerReceiver() {
|
||||||
|
context.registerReceiverAsUser(userSwitcherReceiver, UserHandle.ALL, IntentFilter().apply {
|
||||||
|
intents.forEach {
|
||||||
|
addAction(it)
|
||||||
|
}
|
||||||
|
}, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update(updateUsers: Boolean) {
|
||||||
|
if (updateUsers) {
|
||||||
|
val currentUser = ActivityManager.getCurrentUser()
|
||||||
|
currentUserIds = userManager.getProfiles(currentUser).map { it.id }
|
||||||
|
}
|
||||||
|
bgHandler.post(updateListAndNotifyChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
|
||||||
|
messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
|
||||||
|
}
|
||||||
|
// Notify this callback if we didn't set to listening
|
||||||
|
else if (listening) uiHandler.post(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()) {
|
||||||
|
messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
|
||||||
|
messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addCallback(callback: Callback) {
|
||||||
|
messageHandler.obtainMessage(MSG_ADD_CALLBACK, callback).sendToTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCallback(callback: Callback) {
|
||||||
|
messageHandler.obtainMessage(MSG_REMOVE_CALLBACK, callback).sendToTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if (appOpItem.uid == SYSTEM_UID) return PrivacyItem(type, systemApp)
|
||||||
|
val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid, context)
|
||||||
|
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 (intent?.action in intents) {
|
||||||
|
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 H(
|
||||||
|
private val outerClass: WeakReference<PrivacyItemController>,
|
||||||
|
looper: Looper
|
||||||
|
) : Handler(looper) {
|
||||||
|
override fun handleMessage(msg: Message) {
|
||||||
|
super.handleMessage(msg)
|
||||||
|
when (msg.what) {
|
||||||
|
MSG_UPDATE_LISTENING_STATE -> outerClass.get()?.setListeningState()
|
||||||
|
|
||||||
|
MSG_ADD_CALLBACK -> {
|
||||||
|
if (msg.obj !is PrivacyItemController.Callback) return
|
||||||
|
outerClass.get()?.addCallback(
|
||||||
|
WeakReference(msg.obj as PrivacyItemController.Callback))
|
||||||
|
}
|
||||||
|
|
||||||
|
MSG_REMOVE_CALLBACK -> {
|
||||||
|
if (msg.obj !is PrivacyItemController.Callback) return
|
||||||
|
outerClass.get()?.removeCallback(
|
||||||
|
WeakReference(msg.obj as PrivacyItemController.Callback))
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,24 +32,30 @@ 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;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
import android.util.StatsLog;
|
||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.DisplayCutout;
|
import android.view.DisplayCutout;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
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.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
|
||||||
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;
|
||||||
@@ -57,6 +63,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.PrivacyDialogBuilder;
|
||||||
|
import com.android.systemui.privacy.PrivacyItem;
|
||||||
|
import com.android.systemui.privacy.PrivacyItemController;
|
||||||
|
import com.android.systemui.privacy.PrivacyItemControllerKt;
|
||||||
import com.android.systemui.qs.QSDetail.Callback;
|
import com.android.systemui.qs.QSDetail.Callback;
|
||||||
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
|
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
|
||||||
import com.android.systemui.statusbar.phone.StatusBarIconController;
|
import com.android.systemui.statusbar.phone.StatusBarIconController;
|
||||||
@@ -67,6 +78,8 @@ import com.android.systemui.statusbar.policy.DateView;
|
|||||||
import com.android.systemui.statusbar.policy.NextAlarmController;
|
import com.android.systemui.statusbar.policy.NextAlarmController;
|
||||||
import com.android.systemui.statusbar.policy.ZenModeController;
|
import com.android.systemui.statusbar.policy.ZenModeController;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -108,6 +121,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
private TintedIconManager mIconManager;
|
private TintedIconManager mIconManager;
|
||||||
private TouchAnimator mStatusIconsAlphaAnimator;
|
private TouchAnimator mStatusIconsAlphaAnimator;
|
||||||
private TouchAnimator mHeaderTextContainerAlphaAnimator;
|
private TouchAnimator mHeaderTextContainerAlphaAnimator;
|
||||||
|
private TouchAnimator mPrivacyChipAlphaAnimator;
|
||||||
private DualToneHandler mDualToneHandler;
|
private DualToneHandler mDualToneHandler;
|
||||||
|
|
||||||
private View mSystemIconsView;
|
private View mSystemIconsView;
|
||||||
@@ -127,7 +141,12 @@ 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 boolean mPermissionsHubEnabled;
|
||||||
|
|
||||||
|
private PrivacyItemController mPrivacyItemController;
|
||||||
|
|
||||||
private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() {
|
private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() {
|
||||||
@Override
|
@Override
|
||||||
@@ -137,17 +156,41 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
private boolean mHasTopCutout = false;
|
private boolean mHasTopCutout = false;
|
||||||
|
private boolean mPrivacyChipLogged = false;
|
||||||
|
|
||||||
|
private final DeviceConfig.OnPropertyChangedListener mPropertyListener =
|
||||||
|
new DeviceConfig.OnPropertyChangedListener() {
|
||||||
|
@Override
|
||||||
|
public void onPropertyChanged(String namespace, String name, String value) {
|
||||||
|
if (DeviceConfig.NAMESPACE_PRIVACY.equals(namespace)
|
||||||
|
&& SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED.equals(
|
||||||
|
name)) {
|
||||||
|
mPermissionsHubEnabled = Boolean.valueOf(value);
|
||||||
|
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) {
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -160,6 +203,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
|
mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
|
||||||
mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
|
mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
|
||||||
StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
|
StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
|
||||||
|
// Ignore privacy icons because they show in the space above QQS
|
||||||
|
iconContainer.addIgnoredSlots(getIgnoredIconSlots());
|
||||||
iconContainer.setShouldRestrictIcons(false);
|
iconContainer.setShouldRestrictIcons(false);
|
||||||
mIconManager = new TintedIconManager(iconContainer);
|
mIconManager = new TintedIconManager(iconContainer);
|
||||||
|
|
||||||
@@ -173,6 +218,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mRingerModeIcon = findViewById(R.id.ringer_mode_icon);
|
mRingerModeIcon = findViewById(R.id.ringer_mode_icon);
|
||||||
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);
|
||||||
|
mPrivacyChip = findViewById(R.id.privacy_chip);
|
||||||
|
mPrivacyChip.setOnClickListener(this::onClick);
|
||||||
mCarrierGroup = findViewById(R.id.carrier_group);
|
mCarrierGroup = findViewById(R.id.carrier_group);
|
||||||
|
|
||||||
|
|
||||||
@@ -195,6 +243,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);
|
||||||
@@ -205,6 +254,26 @@ 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 = PrivacyItemControllerKt.isPermissionsHubEnabled();
|
||||||
|
// Change the ignored slots when DeviceConfig flag changes
|
||||||
|
DeviceConfig.addOnPropertyChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
|
||||||
|
mContext.getMainExecutor(), mPropertyListener);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getIgnoredIconSlots() {
|
||||||
|
ArrayList<String> ignored = new ArrayList<>();
|
||||||
|
ignored.add(mContext.getResources().getString(
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatusText() {
|
private void updateStatusText() {
|
||||||
@@ -218,6 +287,21 @@ 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;
|
||||||
|
StatsLog.write(StatsLog.PRIVACY_INDICATORS_INTERACTED,
|
||||||
|
StatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__CHIP_VIEWED);
|
||||||
|
}
|
||||||
|
} 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();
|
||||||
@@ -324,6 +408,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
|
|
||||||
updateStatusIconAlphaAnimator();
|
updateStatusIconAlphaAnimator();
|
||||||
updateHeaderTextContainerAlphaAnimator();
|
updateHeaderTextContainerAlphaAnimator();
|
||||||
|
updatePrivacyChipAlphaAnimator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStatusIconAlphaAnimator() {
|
private void updateStatusIconAlphaAnimator() {
|
||||||
@@ -338,6 +423,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;
|
||||||
@@ -376,6 +467,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mHeaderTextContainerView.setVisibility(INVISIBLE);
|
mHeaderTextContainerView.setVisibility(INVISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mPrivacyChipAlphaAnimator != null) {
|
||||||
|
mPrivacyChip.setExpanded(expansionFraction > 0.5);
|
||||||
|
mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disable(int state1, int state2, boolean animate) {
|
public void disable(int state1, int state2, boolean animate) {
|
||||||
@@ -408,6 +503,21 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
|
mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams();
|
||||||
|
if (cutout != null) {
|
||||||
|
Rect topCutout = cutout.getBoundingRectTop();
|
||||||
|
if (topCutout.isEmpty()) {
|
||||||
|
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);
|
||||||
return super.onApplyWindowInsets(insets);
|
return super.onApplyWindowInsets(insets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,10 +542,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
|
|||||||
mAlarmController.addCallback(this);
|
mAlarmController.addCallback(this);
|
||||||
mContext.registerReceiver(mRingerReceiver,
|
mContext.registerReceiver(mRingerReceiver,
|
||||||
new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
|
new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
|
||||||
|
mPrivacyItemController.addCallback(mPICCallback);
|
||||||
} else {
|
} else {
|
||||||
mZenController.removeCallback(this);
|
mZenController.removeCallback(this);
|
||||||
mAlarmController.removeCallback(this);
|
mAlarmController.removeCallback(this);
|
||||||
|
mPrivacyItemController.removeCallback(mPICCallback);
|
||||||
mContext.unregisterReceiver(mRingerReceiver);
|
mContext.unregisterReceiver(mRingerReceiver);
|
||||||
|
mPrivacyChipLogged = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,6 +566,18 @@ 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
|
||||||
|
PrivacyDialogBuilder builder = mPrivacyChip.getBuilder();
|
||||||
|
if (builder.getAppsAndTypes().size() == 0) return;
|
||||||
|
Handler mUiHandler = new Handler(Looper.getMainLooper());
|
||||||
|
StatsLog.write(StatsLog.PRIVACY_INDICATORS_INTERACTED,
|
||||||
|
StatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__CHIP_CLICKED);
|
||||||
|
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);
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ import com.android.systemui.Dependency;
|
|||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysUiServiceProvider;
|
import com.android.systemui.SysUiServiceProvider;
|
||||||
import com.android.systemui.UiOffloadThread;
|
import com.android.systemui.UiOffloadThread;
|
||||||
|
import com.android.systemui.privacy.PrivacyItem;
|
||||||
|
import com.android.systemui.privacy.PrivacyItemController;
|
||||||
|
import com.android.systemui.privacy.PrivacyItemControllerKt;
|
||||||
|
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.statusbar.CommandQueue;
|
import com.android.systemui.statusbar.CommandQueue;
|
||||||
@@ -62,6 +66,9 @@ import com.android.systemui.statusbar.policy.SensorPrivacyController;
|
|||||||
import com.android.systemui.statusbar.policy.UserInfoController;
|
import com.android.systemui.statusbar.policy.UserInfoController;
|
||||||
import com.android.systemui.statusbar.policy.ZenModeController;
|
import com.android.systemui.statusbar.policy.ZenModeController;
|
||||||
|
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,12 +83,12 @@ public class PhoneStatusBarPolicy
|
|||||||
ZenModeController.Callback,
|
ZenModeController.Callback,
|
||||||
DeviceProvisionedListener,
|
DeviceProvisionedListener,
|
||||||
KeyguardMonitor.Callback,
|
KeyguardMonitor.Callback,
|
||||||
|
PrivacyItemController.Callback,
|
||||||
LocationController.LocationChangeCallback {
|
LocationController.LocationChangeCallback {
|
||||||
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);
|
||||||
|
|
||||||
public static final int LOCATION_STATUS_ICON_ID =
|
public 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;
|
||||||
@@ -95,6 +102,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 Context mContext;
|
private final Context mContext;
|
||||||
@@ -112,6 +121,7 @@ public class PhoneStatusBarPolicy
|
|||||||
private final DeviceProvisionedController mProvisionedController;
|
private final DeviceProvisionedController mProvisionedController;
|
||||||
private final KeyguardMonitor mKeyguardMonitor;
|
private final KeyguardMonitor mKeyguardMonitor;
|
||||||
private final LocationController mLocationController;
|
private final LocationController mLocationController;
|
||||||
|
private final PrivacyItemController mPrivacyItemController;
|
||||||
private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
|
private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
|
||||||
private final SensorPrivacyController mSensorPrivacyController;
|
private final SensorPrivacyController mSensorPrivacyController;
|
||||||
|
|
||||||
@@ -144,6 +154,7 @@ public class PhoneStatusBarPolicy
|
|||||||
mProvisionedController = Dependency.get(DeviceProvisionedController.class);
|
mProvisionedController = Dependency.get(DeviceProvisionedController.class);
|
||||||
mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
|
mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
|
||||||
mLocationController = Dependency.get(LocationController.class);
|
mLocationController = Dependency.get(LocationController.class);
|
||||||
|
mPrivacyItemController = Dependency.get(PrivacyItemController.class);
|
||||||
mSensorPrivacyController = Dependency.get(SensorPrivacyController.class);
|
mSensorPrivacyController = Dependency.get(SensorPrivacyController.class);
|
||||||
|
|
||||||
mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast);
|
mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast);
|
||||||
@@ -159,6 +170,8 @@ public class PhoneStatusBarPolicy
|
|||||||
mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
|
mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
|
||||||
mSlotDataSaver = context.getString(com.android.internal.R.string.status_bar_data_saver);
|
mSlotDataSaver = context.getString(com.android.internal.R.string.status_bar_data_saver);
|
||||||
mSlotLocation = context.getString(com.android.internal.R.string.status_bar_location);
|
mSlotLocation = context.getString(com.android.internal.R.string.status_bar_location);
|
||||||
|
mSlotMicrophone = context.getString(com.android.internal.R.string.status_bar_microphone);
|
||||||
|
mSlotCamera = context.getString(com.android.internal.R.string.status_bar_camera);
|
||||||
mSlotSensorsOff = context.getString(com.android.internal.R.string.status_bar_sensors_off);
|
mSlotSensorsOff = context.getString(com.android.internal.R.string.status_bar_sensors_off);
|
||||||
|
|
||||||
// listen for broadcasts
|
// listen for broadcasts
|
||||||
@@ -218,6 +231,13 @@ public class PhoneStatusBarPolicy
|
|||||||
context.getString(R.string.accessibility_data_saver_on));
|
context.getString(R.string.accessibility_data_saver_on));
|
||||||
mIconController.setIconVisibility(mSlotDataSaver, false);
|
mIconController.setIconVisibility(mSlotDataSaver, false);
|
||||||
|
|
||||||
|
// privacy items
|
||||||
|
mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(),
|
||||||
|
PrivacyType.TYPE_MICROPHONE.getName(mContext));
|
||||||
|
mIconController.setIconVisibility(mSlotMicrophone, false);
|
||||||
|
mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(),
|
||||||
|
PrivacyType.TYPE_CAMERA.getName(mContext));
|
||||||
|
mIconController.setIconVisibility(mSlotCamera, false);
|
||||||
mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
|
mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
|
||||||
mContext.getString(R.string.accessibility_location_active));
|
mContext.getString(R.string.accessibility_location_active));
|
||||||
mIconController.setIconVisibility(mSlotLocation, false);
|
mIconController.setIconVisibility(mSlotLocation, false);
|
||||||
@@ -237,6 +257,7 @@ public class PhoneStatusBarPolicy
|
|||||||
mNextAlarmController.addCallback(mNextAlarmCallback);
|
mNextAlarmController.addCallback(mNextAlarmCallback);
|
||||||
mDataSaver.addCallback(this);
|
mDataSaver.addCallback(this);
|
||||||
mKeyguardMonitor.addCallback(this);
|
mKeyguardMonitor.addCallback(this);
|
||||||
|
mPrivacyItemController.addCallback(this);
|
||||||
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
|
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
|
||||||
mLocationController.addCallback(this);
|
mLocationController.addCallback(this);
|
||||||
|
|
||||||
@@ -580,9 +601,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 (!PrivacyItemControllerKt.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.
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
|
|||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import android.app.AppOpsManager;
|
import android.app.AppOpsManager;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
@@ -53,6 +55,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
|||||||
private static final String TEST_PACKAGE_NAME = "test";
|
private static final String TEST_PACKAGE_NAME = "test";
|
||||||
private static final int TEST_UID = UserHandle.getUid(0, 0);
|
private static final int TEST_UID = UserHandle.getUid(0, 0);
|
||||||
private static final int TEST_UID_OTHER = UserHandle.getUid(1, 0);
|
private static final int TEST_UID_OTHER = UserHandle.getUid(1, 0);
|
||||||
|
private static final int TEST_UID_NON_USER_SENSITIVE = UserHandle.getUid(2, 0);
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private AppOpsManager mAppOpsManager;
|
private AppOpsManager mAppOpsManager;
|
||||||
@@ -71,6 +74,18 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
|||||||
|
|
||||||
getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager);
|
getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager);
|
||||||
|
|
||||||
|
// All permissions of TEST_UID and TEST_UID_OTHER are user sensitive. None of
|
||||||
|
// TEST_UID_NON_USER_SENSITIVE are user sensitive.
|
||||||
|
getContext().setMockPackageManager(mPackageManager);
|
||||||
|
when(mPackageManager.getPermissionFlags(anyString(), anyString(),
|
||||||
|
eq(UserHandle.getUserHandleForUid(TEST_UID)))).thenReturn(
|
||||||
|
PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED);
|
||||||
|
when(mPackageManager.getPermissionFlags(anyString(), anyString(),
|
||||||
|
eq(UserHandle.getUserHandleForUid(TEST_UID_OTHER)))).thenReturn(
|
||||||
|
PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED);
|
||||||
|
when(mPackageManager.getPermissionFlags(anyString(), anyString(),
|
||||||
|
eq(UserHandle.getUserHandleForUid(TEST_UID_NON_USER_SENSITIVE)))).thenReturn(0);
|
||||||
|
|
||||||
mController = new AppOpsControllerImpl(mContext, Dependency.get(Dependency.BG_LOOPER));
|
mController = new AppOpsControllerImpl(mContext, Dependency.get(Dependency.BG_LOOPER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +177,14 @@ public class AppOpsControllerTest extends SysuiTestCase {
|
|||||||
mController.getActiveAppOpsForUser(UserHandle.getUserId(TEST_UID_OTHER)).size());
|
mController.getActiveAppOpsForUser(UserHandle.getUserId(TEST_UID_OTHER)).size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nonUserSensitiveOpsAreIgnored() {
|
||||||
|
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
|
||||||
|
TEST_UID_NON_USER_SENSITIVE, TEST_PACKAGE_NAME, true);
|
||||||
|
assertEquals(0, mController.getActiveAppOpsForUser(
|
||||||
|
UserHandle.getUserId(TEST_UID_NON_USER_SENSITIVE)).size());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void opNotedScheduledForRemoval() {
|
public void opNotedScheduledForRemoval() {
|
||||||
mController.setBGHandler(mMockHandler);
|
mController.setBGHandler(mMockHandler);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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 PrivacyDialogBuilderTest : SysuiTestCase() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TEST_UID = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGenerateAppsList() {
|
||||||
|
val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
|
||||||
|
"Bar", TEST_UID, context))
|
||||||
|
val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
|
||||||
|
"Bar", TEST_UID, context))
|
||||||
|
val foo0 = PrivacyItem(Privacy.TYPE_MICROPHONE, PrivacyApplication(
|
||||||
|
"Foo", TEST_UID, context))
|
||||||
|
val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
|
||||||
|
"Baz", TEST_UID, context))
|
||||||
|
|
||||||
|
val items = listOf(bar2, foo0, baz1, bar3)
|
||||||
|
|
||||||
|
val textBuilder = PrivacyDialogBuilder(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, context))
|
||||||
|
val appMicrophone =
|
||||||
|
PrivacyItem(PrivacyType.TYPE_MICROPHONE,
|
||||||
|
PrivacyApplication("Microphone", TEST_UID, context))
|
||||||
|
val appLocation =
|
||||||
|
PrivacyItem(PrivacyType.TYPE_LOCATION,
|
||||||
|
PrivacyApplication("Location", TEST_UID, context))
|
||||||
|
|
||||||
|
val items = listOf(appLocation, appMicrophone, appCamera)
|
||||||
|
val textBuilder = PrivacyDialogBuilder(context, items)
|
||||||
|
val appList = textBuilder.appsAndTypes.map { it.first }.map { it.packageName }
|
||||||
|
assertEquals(listOf("Camera", "Microphone", "Location"), appList)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.Handler
|
||||||
|
import android.os.UserHandle
|
||||||
|
import android.os.UserManager
|
||||||
|
import android.provider.DeviceConfig
|
||||||
|
import android.provider.Settings.RESET_MODE_PACKAGE_DEFAULTS
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import android.testing.TestableLooper
|
||||||
|
import android.testing.TestableLooper.RunWithLooper
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
|
||||||
|
import com.android.systemui.Dependency
|
||||||
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.appops.AppOpItem
|
||||||
|
import com.android.systemui.appops.AppOpsController
|
||||||
|
import org.hamcrest.Matchers.hasItem
|
||||||
|
import org.hamcrest.Matchers.not
|
||||||
|
import org.hamcrest.Matchers.nullValue
|
||||||
|
import org.junit.After
|
||||||
|
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.any
|
||||||
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
|
import org.mockito.ArgumentMatchers.anyList
|
||||||
|
import org.mockito.ArgumentMatchers.eq
|
||||||
|
import org.mockito.Captor
|
||||||
|
import org.mockito.Mock
|
||||||
|
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.spy
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var appOpsController: AppOpsController
|
||||||
|
@Mock
|
||||||
|
private lateinit var callback: PrivacyItemController.Callback
|
||||||
|
@Mock
|
||||||
|
private lateinit var userManager: UserManager
|
||||||
|
@Captor
|
||||||
|
private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
|
||||||
|
@Captor
|
||||||
|
private lateinit var argCaptorCallback: ArgumentCaptor<AppOpsController.Callback>
|
||||||
|
|
||||||
|
private lateinit var testableLooper: TestableLooper
|
||||||
|
private lateinit var privacyItemController: PrivacyItemController
|
||||||
|
private lateinit var handler: Handler
|
||||||
|
|
||||||
|
fun PrivacyItemController(context: Context) =
|
||||||
|
PrivacyItemController(context, appOpsController, handler, handler)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
testableLooper = TestableLooper.get(this)
|
||||||
|
handler = Handler(testableLooper.looper)
|
||||||
|
|
||||||
|
appOpsController = mDependency.injectMockDependency(AppOpsController::class.java)
|
||||||
|
mDependency.injectTestDependency(Dependency.BG_HANDLER, handler)
|
||||||
|
mDependency.injectTestDependency(Dependency.MAIN_HANDLER, handler)
|
||||||
|
mContext.addMockSystemService(UserManager::class.java, userManager)
|
||||||
|
mContext.getOrCreateTestableResources().addOverride(R.string.device_services,
|
||||||
|
DEVICE_SERVICES_STRING)
|
||||||
|
DeviceConfig.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
DeviceConfig.resetToDefaults(RESET_MODE_PACKAGE_DEFAULTS, DeviceConfig.NAMESPACE_PRIVACY)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSetListeningTrueByAddingCallback() {
|
||||||
|
privacyItemController.addCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
|
||||||
|
any(AppOpsController.Callback::class.java))
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(callback).privacyChanged(anyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSetListeningFalseByRemovingLastCallback() {
|
||||||
|
privacyItemController.addCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(appOpsController, never()).removeCallback(any(IntArray::class.java),
|
||||||
|
any(AppOpsController.Callback::class.java))
|
||||||
|
privacyItemController.removeCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
|
||||||
|
any(AppOpsController.Callback::class.java))
|
||||||
|
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)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(callback).privacyChanged(capture(argCaptor))
|
||||||
|
assertEquals(1, argCaptor.value.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSystemApps() {
|
||||||
|
doReturn(listOf(AppOpItem(AppOpsManager.OP_COARSE_LOCATION, SYSTEM_UID, TEST_PACKAGE_NAME,
|
||||||
|
0))).`when`(appOpsController).getActiveAppOpsForUser(anyInt())
|
||||||
|
privacyItemController.addCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(callback).privacyChanged(capture(argCaptor))
|
||||||
|
assertEquals(1, argCaptor.value.size)
|
||||||
|
assertEquals(context.getString(R.string.device_services),
|
||||||
|
argCaptor.value[0].application.applicationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRegisterReceiver_allUsers() {
|
||||||
|
val spiedContext = spy(mContext)
|
||||||
|
val itemController = PrivacyItemController(spiedContext)
|
||||||
|
itemController.addCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(spiedContext, atLeastOnce()).registerReceiverAsUser(
|
||||||
|
eq(itemController.userSwitcherReceiver), eq(UserHandle.ALL), any(), eq(null),
|
||||||
|
eq(null))
|
||||||
|
verify(spiedContext, never()).unregisterReceiver(eq(itemController.userSwitcherReceiver))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReceiver_ACTION_USER_FOREGROUND() {
|
||||||
|
privacyItemController.userSwitcherReceiver.onReceive(context,
|
||||||
|
Intent(Intent.ACTION_USER_FOREGROUND))
|
||||||
|
verify(userManager).getProfiles(anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReceiver_ACTION_MANAGED_PROFILE_ADDED() {
|
||||||
|
privacyItemController.userSwitcherReceiver.onReceive(context,
|
||||||
|
Intent(Intent.ACTION_MANAGED_PROFILE_ADDED))
|
||||||
|
verify(userManager).getProfiles(anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReceiver_ACTION_MANAGED_PROFILE_REMOVED() {
|
||||||
|
privacyItemController.userSwitcherReceiver.onReceive(context,
|
||||||
|
Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED))
|
||||||
|
verify(userManager).getProfiles(anyInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAddMultipleCallbacks() {
|
||||||
|
val otherCallback = mock(PrivacyItemController.Callback::class.java)
|
||||||
|
privacyItemController.addCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(callback).privacyChanged(anyList())
|
||||||
|
|
||||||
|
privacyItemController.addCallback(otherCallback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
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)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
reset(callback)
|
||||||
|
reset(otherCallback)
|
||||||
|
|
||||||
|
verify(appOpsController).addCallback(any<IntArray>(), capture(argCaptorCallback))
|
||||||
|
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
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)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
reset(callback)
|
||||||
|
reset(otherCallback)
|
||||||
|
|
||||||
|
verify(appOpsController).addCallback(any<IntArray>(), capture(argCaptorCallback))
|
||||||
|
privacyItemController.removeCallback(callback)
|
||||||
|
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
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)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
|
||||||
|
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, mContext)))
|
||||||
|
privacyItemController.privacyList = list
|
||||||
|
val privacyList = privacyItemController.privacyList
|
||||||
|
assertEquals(list, privacyList)
|
||||||
|
assertTrue(list !== privacyList)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNotListeningWhenIndicatorsDisabled() {
|
||||||
|
privacyItemController.devicePropertyChangedListener.onPropertyChanged(
|
||||||
|
DeviceConfig.NAMESPACE_PRIVACY,
|
||||||
|
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
|
||||||
|
"false")
|
||||||
|
privacyItemController.addCallback(callback)
|
||||||
|
testableLooper.processAllMessages()
|
||||||
|
verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
|
||||||
|
any(AppOpsController.Callback::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user