diff options
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 |