diff options
19 files changed, 1235 insertions, 11 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index d238d0eb916d..ea3d2de13ce6 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -120,6 +120,13 @@ public final class SystemUiDeviceConfigFlags { */ public static final String HASH_SALT_MAX_DAYS = "hash_salt_max_days"; + // Flag related to Privacy Indicators + + /** + * Whether the Permissions Hub is showing. + */ + public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_2_enabled"; + // Flags related to Assistant /** diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml index cb321cdc6c4d..8359dac6a30f 100644 --- a/packages/CarSystemUI/res/values/dimens.xml +++ b/packages/CarSystemUI/res/values/dimens.xml @@ -81,6 +81,21 @@ <dimen name="car_keyline_2">96dp</dimen> <dimen name="car_keyline_3">128dp</dimen> + <!-- Height of icons in Ongoing App Ops dialog. Both App Op icon and application icon --> + <dimen name="ongoing_appops_dialog_icon_height">48dp</dimen> + <!-- Margin between text lines in Ongoing App Ops dialog --> + <dimen name="ongoing_appops_dialog_text_margin">15dp</dimen> + <!-- Padding around Ongoing App Ops dialog content --> + <dimen name="ongoing_appops_dialog_content_padding">24dp</dimen> + <!-- Margins around the Ongoing App Ops chip. In landscape, the side margins are 0 --> + <dimen name="ongoing_appops_chip_margin">12dp</dimen> + <!-- Start and End padding for Ongoing App Ops chip --> + <dimen name="ongoing_appops_chip_side_padding">6dp</dimen> + <!-- Padding between background of Ongoing App Ops chip and content --> + <dimen name="ongoing_appops_chip_bg_padding">4dp</dimen> + <!-- Radius of Ongoing App Ops chip corners --> + <dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen> + <!-- Car volume dimens. --> <dimen name="car_volume_item_icon_size">@dimen/car_primary_icon_size</dimen> <dimen name="car_volume_item_height">@*android:dimen/car_single_line_list_item_height</dimen> diff --git a/packages/SystemUI/res/drawable/privacy_chip_bg.xml b/packages/SystemUI/res/drawable/privacy_chip_bg.xml new file mode 100644 index 000000000000..827cf4a9d3b6 --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_chip_bg.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml new file mode 100644 index 000000000000..3c306322d21f --- /dev/null +++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml index be86e5f5abc5..3c7480181877 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml @@ -14,7 +14,7 @@ ** See the License for the specific language governing permissions and ** limitations under the License. --> -<FrameLayout +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:systemui="http://schemas.android.com/apk/res-auto" android:id="@+id/quick_status_bar_system_icons" @@ -27,6 +27,13 @@ android:clickable="true" android:paddingTop="@dimen/status_bar_padding_top" > + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="horizontal" + android:gravity="center_vertical|start" > + <com.android.systemui.statusbar.policy.Clock android:id="@+id/clock" android:layout_width="wrap_content" @@ -38,5 +45,23 @@ android:singleLine="true" android:textAppearance="@style/TextAppearance.StatusBar.Clock" systemui:showDark="false" /> + </LinearLayout> + + <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" /> -</FrameLayout> + </LinearLayout> +</LinearLayout> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 72f623e5fcab..5e5df6bfd988 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -510,6 +510,8 @@ <item>com.android.systemui</item> </string-array> + <integer name="ongoing_appops_dialog_max_apps">5</integer> + <!-- Launcher package name for overlaying icons. --> <string name="launcher_overlayable_package" translatable="false">com.android.launcher3</string> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index eb8758c0d921..5984d8d3322e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1175,6 +1175,23 @@ <!-- How much into a DisplayCutout's bounds we can go, on each side --> <dimen name="display_cutout_margin_consumption">0px</dimen> + + <!-- Height of the Ongoing App Ops chip --> + <dimen name="ongoing_appops_chip_height">32dp</dimen> + <!-- Padding between background of Ongoing App Ops chip and content --> + <dimen name="ongoing_appops_chip_bg_padding">8dp</dimen> + <!-- Side padding between background of Ongoing App Ops chip and content --> + <dimen name="ongoing_appops_chip_side_padding">8dp</dimen> + <!-- Margin between icons of Ongoing App Ops chip when QQS--> + <dimen name="ongoing_appops_chip_icon_margin_collapsed">0dp</dimen> + <!-- Margin between icons of Ongoing App Ops chip when QS--> + <dimen name="ongoing_appops_chip_icon_margin_expanded">2dp</dimen> + <!-- Icon size of Ongoing App Ops chip --> + <dimen name="ongoing_appops_chip_icon_size">@dimen/status_bar_icon_drawing_size</dimen> + <!-- Radius of Ongoing App Ops chip corners --> + <dimen name="ongoing_appops_chip_bg_corner_radius">16dp</dimen> + + <!-- How much each bubble is elevated. --> <dimen name="bubble_elevation">1dp</dimen> <!-- How much the bubble flyout text container is elevated. --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f51cffcdfb5c..28c1b02fc415 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2617,6 +2617,27 @@ app for debugging. Will not be seen by users. [CHAR LIMIT=20] --> <string name="heap_dump_tile_name">Dump SysUI Heap</string> + <!-- Content description for ongoing privacy chip. Use with a single app [CHAR LIMIT=NONE]--> + <string name="ongoing_privacy_chip_content_single_app"><xliff:g id="app" example="Example App">%1$s</xliff:g> is using your <xliff:g id="types_list" example="camera, location">%2$s</xliff:g>.</string> + + <!-- Content description for ongoing privacy chip. Use with multiple apps [CHAR LIMIT=NONE]--> + <string name="ongoing_privacy_chip_content_multiple_apps">Applications are using your <xliff:g id="types_list" example="camera, location">%s</xliff:g>.</string> + + <!-- Separator for types. Include spaces before and after if needed [CHAR LIMIT=10] --> + <string name="ongoing_privacy_dialog_separator">,\u0020</string> + + <!-- Separator for types, before last type. Include spaces before and after if needed [CHAR LIMIT=10] --> + <string name="ongoing_privacy_dialog_last_separator">\u0020and\u0020</string> + + <!-- Text for camera app op [CHAR LIMIT=20]--> + <string name="privacy_type_camera">camera</string> + + <!-- Text for location app op [CHAR LIMIT=20]--> + <string name="privacy_type_location">location</string> + + <!-- Text for microphone app op [CHAR LIMIT=20]--> + <string name="privacy_type_microphone">microphone</string> + <!-- Text for the quick setting tile for sensor privacy [CHAR LIMIT=30] --> <string name="sensor_privacy_mode">Sensors off</string> diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 02d2b8e4ef0f..59580bbf1ae2 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -54,6 +54,7 @@ import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.PowerUI; +import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.recents.Recents; import com.android.systemui.screenrecord.RecordingController; @@ -294,6 +295,7 @@ public class Dependency { @Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager; @Inject Lazy<AutoHideController> mAutoHideController; @Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener; + @Inject Lazy<PrivacyItemController> mPrivacyItemController; @Inject @Background Lazy<Looper> mBgLooper; @Inject @Background Lazy<Handler> mBgHandler; @Inject @Main Lazy<Looper> mMainLooper; @@ -491,6 +493,7 @@ public class Dependency { mProviders.put(ForegroundServiceNotificationListener.class, mForegroundServiceNotificationListener::get); mProviders.put(ClockManager.class, mClockManager::get); + mProviders.put(PrivacyItemController.class, mPrivacyItemController::get); mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get); mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get); mProviders.put(PackageManagerWrapper.class, mPackageManagerWrapper::get); diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index 941de2dc63ec..fc7cc7ee55d3 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -57,7 +57,6 @@ public class AppOpsControllerImpl implements AppOpsController, private static final long NOTED_OP_TIME_DELAY_MS = 5000; private static final String TAG = "AppOpsControllerImpl"; private static final boolean DEBUG = false; - private final Context mContext; private final AppOpsManager mAppOps; private H mBGHandler; @@ -83,7 +82,6 @@ public class AppOpsControllerImpl implements AppOpsController, Context context, @Background Looper bgLooper, DumpManager dumpManager) { - mContext = context; mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mBGHandler = new H(bgLooper); final int numOps = OPS.length; diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt new file mode 100644 index 000000000000..48769cda8481 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt @@ -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) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt new file mode 100644 index 000000000000..1d2e74703b42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt @@ -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() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt new file mode 100644 index 000000000000..1f24fde1377e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt @@ -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 +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt new file mode 100644 index 000000000000..3da1363f2a56 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt new file mode 100644 index 000000000000..8001ecc9697d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -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)) + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b07b1a9561ff..a559a5467663 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -31,7 +31,9 @@ import android.graphics.Color; import android.graphics.Rect; import android.media.AudioManager; import android.os.Handler; +import android.os.Looper; import android.provider.AlarmClock; +import android.provider.DeviceConfig; import android.provider.Settings; import android.service.notification.ZenModeConfig; import android.text.format.DateUtils; @@ -46,7 +48,9 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.RelativeLayout; +import android.widget.Space; import android.widget.TextView; import androidx.annotation.NonNull; @@ -55,6 +59,8 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.internal.logging.UiEventLogger; import com.android.settingslib.Utils; import com.android.systemui.BatteryMeterView; import com.android.systemui.DualToneHandler; @@ -63,6 +69,11 @@ import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.privacy.OngoingPrivacyChip; +import com.android.systemui.privacy.PrivacyChipBuilder; +import com.android.systemui.privacy.PrivacyChipEvent; +import com.android.systemui.privacy.PrivacyItem; +import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.qs.QSDetail.Callback; import com.android.systemui.qs.carrier.QSCarrierGroup; import com.android.systemui.statusbar.CommandQueue; @@ -101,7 +112,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0; public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; - private final Handler mHandler = new Handler(); private final NextAlarmController mAlarmController; private final ZenModeController mZenController; private final StatusBarIconController mStatusBarIconController; @@ -140,9 +150,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements private View mRingerContainer; private Clock mClockView; private DateView mDateView; + private OngoingPrivacyChip mPrivacyChip; + private Space mSpace; private BatteryMeterView mBatteryRemainingIcon; private RingerModeTracker mRingerModeTracker; + private boolean mPermissionsHubEnabled; + private PrivacyItemController mPrivacyItemController; + private final UiEventLogger mUiEventLogger; // Used for RingerModeTracker private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); @@ -156,22 +171,49 @@ public class QuickStatusBarHeader extends RelativeLayout implements private int mCutOutPaddingRight; private float mExpandedHeaderAlpha = 1.0f; private float mKeyguardExpansionFraction; + private boolean mPrivacyChipLogged = false; + + private final DeviceConfig.OnPropertiesChangedListener mPropertiesListener = + new DeviceConfig.OnPropertiesChangedListener() { + @Override + public void onPropertiesChanged(DeviceConfig.Properties properties) { + if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) + && properties.getKeyset() + .contains(SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) { + mPermissionsHubEnabled = properties.getBoolean( + SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false); + StatusIconContainer iconContainer = findViewById(R.id.statusIcons); + iconContainer.setIgnoredSlots(getIgnoredIconSlots()); + } + } + }; + + private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() { + @Override + public void privacyChanged(List<PrivacyItem> privacyItems) { + mPrivacyChip.setPrivacyList(privacyItems); + setChipVisibility(!privacyItems.isEmpty()); + } + }; @Inject public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, NextAlarmController nextAlarmController, ZenModeController zenModeController, StatusBarIconController statusBarIconController, - ActivityStarter activityStarter, - CommandQueue commandQueue, RingerModeTracker ringerModeTracker) { + ActivityStarter activityStarter, PrivacyItemController privacyItemController, + CommandQueue commandQueue, RingerModeTracker ringerModeTracker, + UiEventLogger uiEventLogger) { super(context, attrs); mAlarmController = nextAlarmController; mZenController = zenModeController; mStatusBarIconController = statusBarIconController; mActivityStarter = activityStarter; + mPrivacyItemController = privacyItemController; mDualToneHandler = new DualToneHandler( new ContextThemeWrapper(context, R.style.QSHeaderTheme)); mCommandQueue = commandQueue; mRingerModeTracker = ringerModeTracker; + mUiEventLogger = uiEventLogger; } @Override @@ -198,8 +240,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements mRingerModeTextView = findViewById(R.id.ringer_mode_text); mRingerContainer = findViewById(R.id.ringer_container); mRingerContainer.setOnClickListener(this::onClick); + mPrivacyChip = findViewById(R.id.privacy_chip); + mPrivacyChip.setOnClickListener(this::onClick); mCarrierGroup = findViewById(R.id.carrier_group); + updateResources(); Rect tintArea = new Rect(0, 0, 0, 0); @@ -219,6 +264,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements mClockView = findViewById(R.id.clock); mClockView.setOnClickListener(this); mDateView = findViewById(R.id.date); + mSpace = findViewById(R.id.space); // Tint for the battery icons are handled in setupHost() mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon); @@ -229,6 +275,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); mRingerModeTextView.setSelected(true); mNextAlarmTextView.setSelected(true); + + mPermissionsHubEnabled = mPrivacyItemController.isPermissionsHubEnabled(); } public QuickQSPanel getHeaderQsPanel() { @@ -241,6 +289,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements com.android.internal.R.string.status_bar_camera)); ignored.add(mContext.getResources().getString( com.android.internal.R.string.status_bar_microphone)); + if (mPermissionsHubEnabled) { + ignored.add(mContext.getResources().getString( + com.android.internal.R.string.status_bar_location)); + } return ignored; } @@ -256,6 +308,20 @@ public class QuickStatusBarHeader extends RelativeLayout implements } } + private void setChipVisibility(boolean chipVisible) { + if (chipVisible && mPermissionsHubEnabled) { + mPrivacyChip.setVisibility(View.VISIBLE); + // Makes sure that the chip is logged as viewed at most once each time QS is opened + // mListening makes sure that the callback didn't return after the user closed QS + if (!mPrivacyChipLogged && mListening) { + mPrivacyChipLogged = true; + mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW); + } + } else { + mPrivacyChip.setVisibility(View.GONE); + } + } + private boolean updateRingerStatus() { boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; CharSequence originalRingerText = mRingerModeTextView.getText(); @@ -363,6 +429,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); + updatePrivacyChipAlphaAnimator(); } private void updateStatusIconAlphaAnimator() { @@ -377,6 +444,12 @@ public class QuickStatusBarHeader extends RelativeLayout implements .build(); } + private void updatePrivacyChipAlphaAnimator() { + mPrivacyChipAlphaAnimator = new TouchAnimator.Builder() + .addFloat(mPrivacyChip, "alpha", 1, 0, 1) + .build(); + } + public void setExpanded(boolean expanded) { if (mExpanded == expanded) return; mExpanded = expanded; @@ -415,6 +488,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements mHeaderTextContainerView.setVisibility(INVISIBLE); } } + if (mPrivacyChipAlphaAnimator != null) { + mPrivacyChip.setExpanded(expansionFraction > 0.5); + mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction); + } if (expansionFraction < 1 && expansionFraction > 0.99) { if (mHeaderQsPanel.switchTileLayout()) { updateResources(); @@ -442,6 +519,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements }); mStatusBarIconController.addIconGroup(mIconManager); requestApplyInsets(); + // Change the ignored slots when DeviceConfig flag changes + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, + mContext.getMainExecutor(), mPropertiesListener); } @Override @@ -453,6 +533,31 @@ public class QuickStatusBarHeader extends RelativeLayout implements Pair<Integer, Integer> padding = StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner( cutout, cornerCutoutPadding, -1); + if (padding == null) { + mSystemIconsView.setPaddingRelative( + getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0, + getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0); + } else { + mSystemIconsView.setPadding(padding.first, 0, padding.second, 0); + + } + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams(); + boolean cornerCutout = cornerCutoutPadding != null + && (cornerCutoutPadding.first == 0 || cornerCutoutPadding.second == 0); + if (cutout != null) { + Rect topCutout = cutout.getBoundingRectTop(); + if (topCutout.isEmpty() || cornerCutout) { + mHasTopCutout = false; + lp.width = 0; + mSpace.setVisibility(View.GONE); + } else { + mHasTopCutout = true; + lp.width = topCutout.width(); + mSpace.setVisibility(View.VISIBLE); + } + } + mSpace.setLayoutParams(lp); + setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE); mCutOutPaddingLeft = padding.first; mCutOutPaddingRight = padding.second; mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top; @@ -496,6 +601,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements setListening(false); mRingerModeTracker.getRingerModeInternal().removeObservers(this); mStatusBarIconController.removeIconGroup(mIconManager); + DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener); super.onDetachedFromWindow(); } @@ -513,10 +619,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements mZenController.addCallback(this); mAlarmController.addCallback(this); mLifecycle.setCurrentState(Lifecycle.State.RESUMED); + mPrivacyItemController.addCallback(mPICCallback); } else { mZenController.removeCallback(this); mAlarmController.removeCallback(this); mLifecycle.setCurrentState(Lifecycle.State.CREATED); + mPrivacyItemController.removeCallback(mPICCallback); + mPrivacyChipLogged = false; } } @@ -534,6 +643,17 @@ public class QuickStatusBarHeader extends RelativeLayout implements mActivityStarter.postStartActivityDismissingKeyguard(new Intent( AlarmClock.ACTION_SHOW_ALARMS), 0); } + } else if (v == mPrivacyChip) { + // Makes sure that the builder is grabbed as soon as the chip is pressed + PrivacyChipBuilder builder = mPrivacyChip.getBuilder(); + if (builder.getAppsAndTypes().size() == 0) return; + Handler mUiHandler = new Handler(Looper.getMainLooper()); + mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK); + mUiHandler.post(() -> { + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0); + mHost.collapsePanels(); + }); } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) { mActivityStarter.postStartActivityDismissingKeyguard(new Intent( Settings.ACTION_SOUND_SETTINGS), 0); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 5bb8fab8a62e..01b6fbf1f361 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -47,6 +47,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; +import com.android.systemui.privacy.PrivacyItem; +import com.android.systemui.privacy.PrivacyItemController; +import com.android.systemui.privacy.PrivacyType; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.screenrecord.RecordingController; @@ -70,6 +73,9 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.time.DateFormatUtil; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; @@ -87,13 +93,13 @@ public class PhoneStatusBarPolicy ZenModeController.Callback, DeviceProvisionedListener, KeyguardStateController.Callback, + PrivacyItemController.Callback, LocationController.LocationChangeCallback, RecordingController.RecordingStateChangeCallback { private static final String TAG = "PhoneStatusBarPolicy"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - static final int LOCATION_STATUS_ICON_ID = - com.android.internal.R.drawable.perm_group_location; + static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId(); private final String mSlotCast; private final String mSlotHotspot; @@ -107,6 +113,8 @@ public class PhoneStatusBarPolicy private final String mSlotHeadset; private final String mSlotDataSaver; private final String mSlotLocation; + private final String mSlotMicrophone; + private final String mSlotCamera; private final String mSlotSensorsOff; private final String mSlotScreenRecord; private final int mDisplayId; @@ -132,6 +140,7 @@ public class PhoneStatusBarPolicy private final DeviceProvisionedController mProvisionedController; private final KeyguardStateController mKeyguardStateController; private final LocationController mLocationController; + private final PrivacyItemController mPrivacyItemController; private final Executor mUiBgExecutor; private final SensorPrivacyController mSensorPrivacyController; private final RecordingController mRecordingController; @@ -162,7 +171,8 @@ public class PhoneStatusBarPolicy RecordingController recordingController, @Nullable TelecomManager telecomManager, @DisplayId int displayId, @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil, - RingerModeTracker ringerModeTracker) { + RingerModeTracker ringerModeTracker, + PrivacyItemController privacyItemController) { mIconController = iconController; mCommandQueue = commandQueue; mBroadcastDispatcher = broadcastDispatcher; @@ -181,6 +191,7 @@ public class PhoneStatusBarPolicy mProvisionedController = deviceProvisionedController; mKeyguardStateController = keyguardStateController; mLocationController = locationController; + mPrivacyItemController = privacyItemController; mSensorPrivacyController = sensorPrivacyController; mRecordingController = recordingController; mUiBgExecutor = uiBgExecutor; @@ -200,6 +211,8 @@ public class PhoneStatusBarPolicy mSlotHeadset = resources.getString(com.android.internal.R.string.status_bar_headset); mSlotDataSaver = resources.getString(com.android.internal.R.string.status_bar_data_saver); mSlotLocation = resources.getString(com.android.internal.R.string.status_bar_location); + mSlotMicrophone = resources.getString(com.android.internal.R.string.status_bar_microphone); + mSlotCamera = resources.getString(com.android.internal.R.string.status_bar_camera); mSlotSensorsOff = resources.getString(com.android.internal.R.string.status_bar_sensors_off); mSlotScreenRecord = resources.getString( com.android.internal.R.string.status_bar_screen_record); @@ -271,6 +284,13 @@ public class PhoneStatusBarPolicy mResources.getString(R.string.accessibility_data_saver_on)); mIconController.setIconVisibility(mSlotDataSaver, false); + // privacy items + mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(), + mResources.getString(PrivacyType.TYPE_MICROPHONE.getNameId())); + mIconController.setIconVisibility(mSlotMicrophone, false); + mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(), + mResources.getString(PrivacyType.TYPE_CAMERA.getNameId())); + mIconController.setIconVisibility(mSlotCamera, false); mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID, mResources.getString(R.string.accessibility_location_active)); mIconController.setIconVisibility(mSlotLocation, false); @@ -294,6 +314,7 @@ public class PhoneStatusBarPolicy mNextAlarmController.addCallback(mNextAlarmCallback); mDataSaver.addCallback(this); mKeyguardStateController.addCallback(this); + mPrivacyItemController.addCallback(this); mSensorPrivacyController.addCallback(mSensorPrivacyListener); mLocationController.addCallback(this); mRecordingController.addCallback(this); @@ -609,9 +630,46 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotDataSaver, isDataSaving); } + @Override // PrivacyItemController.Callback + public void privacyChanged(List<PrivacyItem> privacyItems) { + updatePrivacyItems(privacyItems); + } + + private void updatePrivacyItems(List<PrivacyItem> items) { + boolean showCamera = false; + boolean showMicrophone = false; + boolean showLocation = false; + for (PrivacyItem item : items) { + if (item == null /* b/124234367 */) { + if (DEBUG) { + Log.e(TAG, "updatePrivacyItems - null item found"); + StringWriter out = new StringWriter(); + mPrivacyItemController.dump(null, new PrintWriter(out), null); + Log.e(TAG, out.toString()); + } + continue; + } + switch (item.getPrivacyType()) { + case TYPE_CAMERA: + showCamera = true; + break; + case TYPE_LOCATION: + showLocation = true; + break; + case TYPE_MICROPHONE: + showMicrophone = true; + break; + } + } + + mIconController.setIconVisibility(mSlotCamera, showCamera); + mIconController.setIconVisibility(mSlotMicrophone, showMicrophone); + mIconController.setIconVisibility(mSlotLocation, showLocation); + } + @Override public void onLocationActiveChanged(boolean active) { - updateLocation(); + if (!mPrivacyItemController.isPermissionsHubEnabled()) updateLocation(); } // Updates the status view based on the current state of location requests. diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt new file mode 100644 index 000000000000..dcee5a716ceb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt @@ -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) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt new file mode 100644 index 000000000000..1f7baa9c4fcd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt @@ -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()) + } +}
\ No newline at end of file |