summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/privacy_chip_bg.xml22
-rw-r--r--packages/SystemUI/res/layout/ongoing_privacy_chip.xml44
-rw-r--r--packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml50
-rw-r--r--packages/SystemUI/res/layout/ongoing_privacy_text_item.xml24
-rw-r--r--packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml2
-rw-r--r--packages/SystemUI/res/values/dimens.xml15
-rw-r--r--packages/SystemUI/res/values/strings.xml35
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt152
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt109
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt74
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java30
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt81
13 files changed, 693 insertions, 0 deletions
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..8247c27ff850
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_chip_bg.xml
@@ -0,0 +1,22 @@
+<?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="#bbbbbb" />
+ <padding android:padding="@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..5e952e3c4413
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -0,0 +1,44 @@
+<?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_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/ongoing_appops_chip_margin"
+ android:gravity="center_vertical|end"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/ongoing_appops_chip_side_padding"
+ android:paddingEnd="@dimen/ongoing_appops_chip_side_padding"
+ android:background="@drawable/privacy_chip_bg"
+ android:focusable="true">
+
+ <LinearLayout
+ android:id="@+id/icons_container"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical|start"
+ />
+
+ <TextView
+ android:id="@+id/app_name"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical|end"
+ />
+</com.android.systemui.privacy.OngoingPrivacyChip> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml b/packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml
new file mode 100644
index 000000000000..b5e24a04f85e
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml
@@ -0,0 +1,50 @@
+<?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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fillViewport ="true"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/dialog_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="@dimen/ongoing_appops_dialog_content_padding">
+
+ <LinearLayout
+ android:id="@+id/icons_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/ongoing_appops_dialog_icon_height"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:importantForAccessibility="no"
+ />
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="start"
+ />
+ </LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_text_item.xml b/packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
new file mode 100644
index 000000000000..5595b130e041
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
@@ -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"
+/> \ 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 680112c73c0d..007070e3ffba 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
@@ -46,6 +46,8 @@
android:layout_weight="1"
android:gravity="center_vertical|center_horizontal" />
+ <include layout="@layout/ongoing_privacy_chip" />
+
<com.android.systemui.BatteryMeterView
android:id="@+id/battery"
android:layout_height="match_parent"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f77d9234463e..525421a6870a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -932,4 +932,19 @@
<!-- How much we expand the touchable region of the status bar below the notch to catch touches
that just start below the notch. -->
<dimen name="display_cutout_touchable_region_size">12dp</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>
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d67841213c7e..7d09c0079ae8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2238,4 +2238,39 @@
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>
+
+ <!-- Action on Ongoing Privacy Dialog to open application [CHAR LIMIT=10]-->
+ <string name="ongoing_privacy_dialog_open_app">Open app</string>
+
+ <!-- Action on Ongoing Privacy Dialog to dismiss [CHAR LIMIT=10]-->
+ <string name="ongoing_privacy_dialog_cancel">Cancel</string>
+
+ <!-- Action on Ongoing Privacy Dialog to dismiss [CHAR LIMIT=10]-->
+ <string name="ongoing_privacy_dialog_okay">Okay</string>
+
+ <!-- Action on Ongoing Privacy Dialog to open privacy hub [CHAR LIMIT=10]-->
+ <string name="ongoing_privacy_dialog_open_settings">Settings</string>
+
+ <!-- Text for item in Ongoing Privacy Dialog when only one app is using a particular type of app op [CHAR LIMIT=NONE] -->
+ <string name="ongoing_privacy_dialog_app_item"><xliff:g id="app" example="Example App">%1$s</xliff:g> is using your <xliff:g id="type" example="camera">%2$s</xliff:g> for the last <xliff:g id="time" example="3">%3$d</xliff:g> min</string>
+
+ <!-- Text for item in Ongoing Privacy Dialog when only multiple apps are using a particular type of app op [CHAR LIMIT=NONE] -->
+ <string name="ongoing_privacy_dialog_apps_item"><xliff:g id="apps" example="Camera, Phone">%1$s</xliff:g> are using your <xliff:g id="type" example="camera">%2$s</xliff:g></string>
+
+ <!-- Text for Ongoing Privacy Dialog when a single app is using app ops [CHAR LIMIT=NONE] -->
+ <string name="ongoing_privacy_dialog_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>
+
+ <!-- Text for camera app op [CHAR LIMIT=12]-->
+ <string name="privacy_type_camera">camera</string>
+
+ <!-- Text for location app op [CHAR LIMIT=12]-->
+ <string name="privacy_type_location">location</string>
+
+ <!-- Text for microphone app op [CHAR LIMIT=12]-->
+ <string name="privacy_type_microphone">microphone</string>
</resources>
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..3953139d43fd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.graphics.Color
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.AttributeSet
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.Dependency
+import com.android.systemui.R
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+
+class OngoingPrivacyChip @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttrs: Int = 0,
+ defStyleRes: Int = 0
+) : LinearLayout(context, attrs, defStyleAttrs, defStyleRes) {
+
+ companion object {
+ val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
+ AppOpsManager.OP_RECORD_AUDIO,
+ AppOpsManager.OP_COARSE_LOCATION,
+ AppOpsManager.OP_FINE_LOCATION)
+ }
+
+ private lateinit var appName: TextView
+ private lateinit var iconsContainer: LinearLayout
+ private var privacyList = emptyList<PrivacyItem>()
+ private val appOpsController = Dependency.get(AppOpsController::class.java)
+ private val userManager = context.getSystemService(UserManager::class.java)
+ private val currentUser = ActivityManager.getCurrentUser()
+ private val currentUserIds = userManager.getProfiles(currentUser).map { it.id }
+ private var listening = false
+
+ var builder = PrivacyDialogBuilder(context, privacyList)
+
+ private val callback = object : AppOpsController.Callback {
+ override fun onActiveStateChanged(
+ code: Int,
+ uid: Int,
+ packageName: String,
+ active: Boolean
+ ) {
+ val userId = UserHandle.getUserId(uid)
+ if (userId in currentUserIds) {
+ updatePrivacyList()
+ }
+ }
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ appName = findViewById(R.id.app_name)
+ iconsContainer = findViewById(R.id.icons_container)
+ }
+
+ fun setListening(listen: Boolean) {
+ if (listening == listen) return
+ listening = listen
+ if (listening) {
+ appOpsController.addCallback(OPS, callback)
+ updatePrivacyList()
+ } else {
+ appOpsController.removeCallback(OPS, callback)
+ }
+ }
+
+ private fun updatePrivacyList() {
+ privacyList = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
+ .mapNotNull { toPrivacyItem(it) }
+ builder = PrivacyDialogBuilder(context, privacyList)
+ updateView()
+ }
+
+ 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, context)
+ return PrivacyItem(type, app, appOpItem.timeStarted)
+ }
+
+ // Should only be called if the builder icons or app changed
+ private fun updateView() {
+ fun setIcons(dialogBuilder: PrivacyDialogBuilder, iconsContainer: ViewGroup) {
+ iconsContainer.removeAllViews()
+ dialogBuilder.generateIcons().forEach {
+ it.mutate()
+ it.setTint(Color.WHITE)
+ iconsContainer.addView(ImageView(context).apply {
+ setImageDrawable(it)
+ maxHeight = this@OngoingPrivacyChip.height
+ })
+ }
+ }
+
+ if (privacyList.isEmpty()) {
+ visibility = GONE
+ return
+ } else {
+ generateContentDescription()
+ visibility = VISIBLE
+ setIcons(builder, iconsContainer)
+ appName.visibility = GONE
+ builder.app?.let {
+ appName.apply {
+ setText(it.applicationName)
+ setTextColor(Color.WHITE)
+ visibility = VISIBLE
+ }
+ }
+ }
+ requestLayout()
+ }
+
+ private fun generateContentDescription() {
+ val typesText = builder.generateTypesText()
+ if (builder.app != null) {
+ contentDescription = context.getString(R.string.ongoing_privacy_chip_content_single_app,
+ builder.app?.applicationName, typesText)
+ } else {
+ 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/OngoingPrivacyDialog.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
new file mode 100644
index 000000000000..1d0e16ed3334
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.AlertDialog
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.Dependency
+import com.android.systemui.R
+import com.android.systemui.plugins.ActivityStarter
+
+class OngoingPrivacyDialog constructor(
+ val context: Context,
+ val dialogBuilder: PrivacyDialogBuilder
+) {
+
+ val iconHeight = context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_appops_dialog_icon_height)
+ val textMargin = context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_appops_dialog_text_margin)
+ val iconColor = context.resources.getColor(
+ com.android.internal.R.color.text_color_primary, context.theme)
+
+ fun createDialog(): Dialog {
+ val builder = AlertDialog.Builder(context)
+ .setNeutralButton(R.string.ongoing_privacy_dialog_open_settings, null)
+ if (dialogBuilder.app != null) {
+ builder.setPositiveButton(R.string.ongoing_privacy_dialog_open_app,
+ object : DialogInterface.OnClickListener {
+ val intent = context.packageManager
+ .getLaunchIntentForPackage(dialogBuilder.app.packageName)
+
+ override fun onClick(dialog: DialogInterface?, which: Int) {
+ Dependency.get(ActivityStarter::class.java).startActivity(intent, false)
+ }
+ })
+ builder.setNegativeButton(R.string.ongoing_privacy_dialog_cancel, null)
+ } else {
+ builder.setPositiveButton(R.string.ongoing_privacy_dialog_okay, null)
+ }
+ builder.setView(getContentView())
+ return builder.create()
+ }
+
+ fun getContentView(): View {
+ val layoutInflater = LayoutInflater.from(context)
+ val contentView = layoutInflater.inflate(R.layout.ongoing_privacy_dialog_content, null)
+
+ val iconsContainer = contentView.findViewById(R.id.icons_container) as LinearLayout
+ val textContainer = contentView.findViewById(R.id.text_container) as LinearLayout
+
+ addIcons(dialogBuilder, iconsContainer)
+ val lm = ViewGroup.MarginLayoutParams(
+ ViewGroup.MarginLayoutParams.WRAP_CONTENT,
+ ViewGroup.MarginLayoutParams.WRAP_CONTENT)
+ lm.topMargin = textMargin
+ val now = System.currentTimeMillis()
+ dialogBuilder.generateText(now).forEach {
+ val text = layoutInflater.inflate(R.layout.ongoing_privacy_text_item, null) as TextView
+ text.setText(it)
+ textContainer.addView(text, lm)
+ }
+ return contentView
+ }
+
+ private fun addIcons(dialogBuilder: PrivacyDialogBuilder, iconsContainer: LinearLayout) {
+
+ fun LinearLayout.addIcon(icon: Drawable) {
+ val image = ImageView(context).apply {
+ setImageDrawable(icon.apply {
+ setBounds(0, 0, iconHeight, iconHeight)
+ maxHeight = this@addIcon.height
+ })
+ adjustViewBounds = true
+ }
+ addView(image, LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.MATCH_PARENT)
+ }
+
+ dialogBuilder.generateIcons().forEach {
+ it.mutate()
+ it.setTint(iconColor)
+ iconsContainer.addIcon(it)
+ }
+ dialogBuilder.app.let {
+ it?.icon?.let { iconsContainer.addIcon(it) }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt
new file mode 100644
index 000000000000..2f86f78d7669
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt
@@ -0,0 +1,74 @@
+/*
+ * 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 com.android.systemui.R
+import java.lang.IllegalStateException
+import java.lang.Math.max
+
+class PrivacyDialogBuilder(val context: Context, itemsList: List<PrivacyItem>) {
+ companion object {
+ val MILLIS_IN_MINUTE: Long = 1000 * 60
+ }
+
+ private val itemsByType: Map<PrivacyType, List<PrivacyItem>>
+ val app: PrivacyApplication?
+
+ init {
+ itemsByType = itemsList.groupBy { it.privacyType }
+ val apps = itemsList.map { it.application }.distinct()
+ val singleApp = apps.size == 1
+ app = if (singleApp) apps.get(0) else null
+ }
+
+ private fun buildTextForItem(type: PrivacyType, now: Long): String {
+ val items = itemsByType.getOrDefault(type, emptyList<PrivacyItem>())
+ return when (items.size) {
+ 0 -> throw IllegalStateException("List cannot be empty")
+ 1 -> {
+ val item = items.get(0)
+ val minutesUsed = max(((now - item.timeStarted) / MILLIS_IN_MINUTE).toInt(), 1)
+ context.getString(R.string.ongoing_privacy_dialog_app_item,
+ item.application.applicationName, type.getName(context), minutesUsed)
+ }
+ else -> {
+ val apps = items.map { it.application.applicationName }.joinToString()
+ context.getString(R.string.ongoing_privacy_dialog_apps_item,
+ apps, type.getName(context))
+ }
+ }
+ }
+
+ private fun buildTextForApp(types: Set<PrivacyType>): List<String> {
+ app?.let {
+ val typesText = types.map { it.getName(context) }.sorted().joinToString()
+ return listOf(context.getString(R.string.ongoing_privacy_dialog_single_app,
+ it.applicationName, typesText))
+ } ?: throw IllegalStateException("There has to be a single app")
+ }
+
+ fun generateText(now: Long): List<String> {
+ if (app == null || itemsByType.keys.size == 1) {
+ return itemsByType.keys.map { buildTextForItem(it, now) }
+ } else {
+ return buildTextForApp(itemsByType.keys)
+ }
+ }
+
+ fun generateTypesText() = itemsByType.keys.map { it.getName(context) }.sorted().joinToString()
+
+ fun generateIcons() = itemsByType.keys.map { it.getIcon(context) }
+} \ 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..f4099021a0bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 com.android.systemui.R
+
+typealias Privacy = PrivacyType
+
+enum class PrivacyType(val nameId: Int, val iconId: Int) {
+ TYPE_CAMERA(R.string.privacy_type_camera, com.android.internal.R.drawable.ic_camera),
+ TYPE_LOCATION(R.string.privacy_type_location, R.drawable.stat_sys_location),
+ TYPE_MICROPHONE(R.string.privacy_type_microphone, R.drawable.ic_mic_26dp);
+
+ fun getName(context: Context) = context.resources.getString(nameId)
+
+ fun getIcon(context: Context) = context.resources.getDrawable(iconId, null)
+}
+
+data class PrivacyItem(
+ val privacyType: PrivacyType,
+ val application: PrivacyApplication,
+ val timeStarted: Long
+)
+
+data class PrivacyApplication(val packageName: String, val context: Context) {
+ var icon: Drawable? = null
+ var applicationName: String
+
+ init {
+ try {
+ val app: ApplicationInfo = context.packageManager
+ .getApplicationInfo(packageName, 0)
+ icon = context.packageManager.getApplicationIcon(app)
+ applicationName = context.packageManager.getApplicationLabel(app) as String
+ } catch (e: PackageManager.NameNotFoundException) {
+ applicationName = packageName
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 326df498c984..3ee6195858d6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter;
import android.annotation.ColorInt;
import android.app.ActivityManager;
import android.app.AlarmManager;
+import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -31,6 +32,7 @@ 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.service.notification.ZenModeConfig;
import android.text.format.DateUtils;
@@ -39,6 +41,7 @@ import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.WindowInsets;
+import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
@@ -52,11 +55,14 @@ import com.android.systemui.Dependency;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.privacy.OngoingPrivacyChip;
+import com.android.systemui.privacy.OngoingPrivacyDialog;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
import com.android.systemui.statusbar.phone.StatusIconContainer;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.DarkIconDispatcher;
import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
@@ -118,6 +124,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
private BatteryMeterView mBatteryMeterView;
private Clock mClockView;
private DateView mDateView;
+ private OngoingPrivacyChip mPrivacyChip;
private NextAlarmController mAlarmController;
private ZenModeController mZenController;
@@ -185,6 +192,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mClockView = findViewById(R.id.clock);
mClockView.setOnClickListener(this);
mDateView = findViewById(R.id.date);
+ mPrivacyChip = findViewById(R.id.privacy_chip);
+ mPrivacyChip.setOnClickListener(this);
}
private void updateStatusText() {
@@ -263,6 +272,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
mBatteryMeterView.useWallpaperTextColor(shouldUseWallpaperTextColor);
mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor);
+
+ MarginLayoutParams lm = (MarginLayoutParams) mPrivacyChip.getLayoutParams();
+ int sideMargins = lm.leftMargin;
+ int topBottomMargins = (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
+ ? 0 : sideMargins;
+ lm.setMargins(sideMargins, topBottomMargins, sideMargins, topBottomMargins);
+ mPrivacyChip.setLayoutParams(lm);
}
@Override
@@ -421,6 +437,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
return;
}
mHeaderQsPanel.setListening(listening);
+ mPrivacyChip.setListening(listening);
mListening = listening;
if (listening) {
@@ -443,6 +460,19 @@ public class QuickStatusBarHeader extends RelativeLayout implements
} else if (v == mBatteryMeterView) {
Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(new Intent(
Intent.ACTION_POWER_USAGE_SUMMARY),0);
+ } else if (v == mPrivacyChip) {
+ Handler mUiHandler = new Handler(Looper.getMainLooper());
+ mUiHandler.post(() -> {
+ Dialog mDialog = new OngoingPrivacyDialog(mContext,
+ mPrivacyChip.getBuilder()).createDialog();
+ mDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+ SystemUIDialog.setShowForAllUsers(mDialog, true);
+ SystemUIDialog.registerDismissListener(mDialog);
+ SystemUIDialog.setWindowOnTop(mDialog);
+ mUiHandler.post(() -> mDialog.show());
+ mHost.collapsePanels();
+ });
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt
new file mode 100644
index 000000000000..7204d310a76d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.support.test.filters.SmallTest
+import android.support.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 MILLIS_IN_MINUTE: Long = 1000 * 60
+ val NOW = 4 * MILLIS_IN_MINUTE
+ }
+
+ @Test
+ fun testGenerateText_multipleApps() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", context), 2 * MILLIS_IN_MINUTE)
+ val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
+ "Bar", context), 3 * MILLIS_IN_MINUTE)
+ val foo0 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Foo", context), 0)
+ val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Baz", context), 1 * MILLIS_IN_MINUTE)
+
+ val items = listOf(bar2, foo0, baz1, bar3)
+
+ val textBuilder = PrivacyDialogBuilder(context, items)
+
+ val textList = textBuilder.generateText(NOW)
+ assertEquals(2, textList.size)
+ assertEquals("Bar, Foo, Baz are using your camera", textList[0])
+ assertEquals("Bar is using your location for the last 1 min", textList[1])
+ }
+
+ @Test
+ fun testGenerateText_singleApp() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", context), 0)
+ val bar1 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
+ "Bar", context), 0)
+
+ val items = listOf(bar2, bar1)
+
+ val textBuilder = PrivacyDialogBuilder(context, items)
+ val textList = textBuilder.generateText(NOW)
+ assertEquals(1, textList.size)
+ assertEquals("Bar is using your camera, location", textList[0])
+ }
+
+ @Test
+ fun testGenerateText_singleApp_singleType() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", context), 2 * MILLIS_IN_MINUTE)
+ val items = listOf(bar2)
+ val textBuilder = PrivacyDialogBuilder(context, items)
+ val textList = textBuilder.generateText(NOW)
+ assertEquals(1, textList.size)
+ assertEquals("Bar is using your camera for the last 2 min", textList[0])
+ }
+} \ No newline at end of file