Version 2 of Ongoing Privacy Dialog

Minor changes to colors and layout of chip.

Redesign of dialog using new mocks.

Dialog launches Permission Hub

Test: visual & atest PrivacyDialogBuilderTest
Fixes: 117646163
Bug: 112331475

Change-Id: Ic8008f05fcb139c2581794abbb47c00819c20d7f
This commit is contained in:
Fabian Kozynski
2018-11-02 11:02:11 -04:00
parent 22edbb5e7e
commit ef12449cf8
16 changed files with 310 additions and 188 deletions

View File

@@ -16,7 +16,7 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#bbbbbb" />
<solid android:color="#4a4a4a" />
<padding android:padding="@dimen/ongoing_appops_chip_bg_padding" />
<corners android:radius="@dimen/ongoing_appops_chip_bg_corner_radius" />
</shape>

View File

@@ -18,11 +18,14 @@
<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:layout_width="wrap_content"
android:layout_marginLeft="@dimen/ongoing_appops_chip_margin"
android:layout_marginRight="@dimen/ongoing_appops_chip_margin"
android:layout_marginTop="@dimen/ongoing_appops_top_chip_margin"
android:layout_marginBottom="@dimen/ongoing_appops_top_chip_margin"
android:gravity="center_vertical|center_horizontal"
android:layout_gravity="center_vertical|end"
android:layout_gravity="center_vertical|start"
android:orientation="horizontal"
android:paddingStart="@dimen/ongoing_appops_chip_side_padding"
android:paddingEnd="@dimen/ongoing_appops_chip_side_padding"
@@ -38,12 +41,17 @@
/>
<TextView
android:id="@+id/app_name"
android:id="@+id/text_container"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:lines="1"
android:layout_gravity="center_vertical|end"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:textColor="@color/status_bar_clock_color"
android:layout_marginStart="@dimen/ongoing_appops_chip_icon_margin"
android:layout_marginEnd="@dimen/ongoing_appops_chip_icon_margin"
/>
</com.android.systemui.privacy.OngoingPrivacyChip>

View File

@@ -29,22 +29,30 @@
android:orientation="vertical"
android:padding="@dimen/ongoing_appops_dialog_content_padding">
<LinearLayout
android:id="@+id/icons_container"
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="@dimen/ongoing_appops_dialog_icon_height"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:gravity="center"
android:importantForAccessibility="no"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.AppOpsDialog.Title"
android:textColor="@*android:color/text_color_primary"
android:paddingStart="@dimen/ongoing_appops_dialog_title_padding"
android:paddingEnd="@dimen/ongoing_appops_dialog_title_padding"
android:paddingBottom="@dimen/ongoing_appops_dialog_sep"
/>
<LinearLayout
android:id="@+id/text_container"
android:id="@+id/items_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="start"
/>
<include android:id="@+id/overflow" layout="@layout/ongoing_privacy_dialog_item"
android:visibility="gone" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,53 @@
<?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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
android:orientation="horizontal"
android:layout_marginTop="@dimen/ongoing_appops_dialog_text_margin"
android:focusable="true" >
<ImageView
android:id="@+id/app_icon"
android:layout_height="@dimen/ongoing_appops_dialog_icon_height"
android:layout_width="@dimen/ongoing_appops_dialog_icon_height"
/>
<TextView
android:id="@+id/app_name"
android:layout_height="@dimen/ongoing_appops_dialog_icon_height"
android:layout_width="0dp"
android:layout_weight="1"
android:gravity="bottom|start"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.AppOpsDialog.Item"
android:textColor="@*android:color/text_color_primary"
android:paddingStart="@dimen/ongoing_appops_dialog_text_padding"
android:paddingEnd="@dimen/ongoing_appops_dialog_text_padding"
/>
<LinearLayout
android:id="@+id/icons"
android:layout_height="@dimen/ongoing_appops_dialog_icon_height"
android:layout_width="wrap_content"
android:gravity="end"
android:visibility="gone"
/>
</LinearLayout>

View File

@@ -59,7 +59,7 @@
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical|end">
android:gravity="center_vertical|end" >
<include layout="@layout/ongoing_privacy_chip" />
@@ -67,6 +67,7 @@
android:id="@+id/battery"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:gravity="center_vertical|end" />
android:gravity="center_vertical|end"
android:layout_gravity="center_vertical|end" />
</LinearLayout>
</LinearLayout>

View File

@@ -34,4 +34,5 @@
<bool name="quick_settings_wide">true</bool>
<dimen name="qs_detail_margin_top">0dp</dimen>
<dimen name="qs_paged_tile_layout_padding_bottom">0dp</dimen>
<dimen name="ongoing_appops_top_chip_margin">2dp</dimen>
</resources>

View File

@@ -940,18 +940,34 @@
that just start below the notch. -->
<dimen name="display_cutout_touchable_region_size">12dp</dimen>
<!-- Padding below Ongoing App Ops dialog title -->
<dimen name="ongoing_appops_dialog_sep">16dp</dimen>
<!--Padding around text items in Ongoing App Ops dialog -->
<dimen name="ongoing_appops_dialog_text_padding">16dp</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>
<dimen name="ongoing_appops_dialog_icon_height">28dp</dimen>
<!-- Margin between text lines in Ongoing App Ops dialog -->
<dimen name="ongoing_appops_dialog_text_margin">15dp</dimen>
<!-- Side padding of title in Ongoing App Ops dialog -->
<dimen name="ongoing_appops_dialog_title_padding">10dp</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 -->
<!-- Side margins around the Ongoing App Ops chip-->
<dimen name="ongoing_appops_chip_margin">12dp</dimen>
<!-- Top and bottom margins around the Ongoing App Ops chip -->
<dimen name="ongoing_appops_top_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>
<dimen name="ongoing_appops_chip_bg_padding">0dp</dimen>
<!-- Margin between icons of Ongoing App Ops chip -->
<dimen name="ongoing_appops_chip_icon_margin">4dp</dimen>
<!-- Icon size of Ongoing App Ops chip -->
<dimen name="ongoing_appops_chip_icon_size">18dp</dimen>
<!-- Radius of Ongoing App Ops chip corners -->
<dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen>
<!-- Text size for Ongoing App Ops dialog title -->
<dimen name="ongoing_appops_dialog_title_size">24sp</dimen>
<!-- Text size for Ongoing App Ops dialog items -->
<dimen name="ongoing_appops_dialog_item_size">20sp</dimen>
</resources>

View File

@@ -2250,39 +2250,48 @@
app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
<string name="heap_dump_tile_name">Dump SysUI Heap</string>
<!-- Text on chip for multiple apps using a single app op [CHAR LIMIT=10] -->
<string name="ongoing_privacy_chip_multiple_apps"><xliff:g id="num_apps" example="3">%d</xliff:g> apps</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>
<!-- Content description for ongoing privacy chip. Use with multiple apps using same app op[CHAR LIMIT=NONE]-->
<string name="ongoing_privacy_chip_content_multiple_apps_single_op"><xliff:g id="num_apps" example="3">%1$d</xliff:g> applications are using your <xliff:g id="type" example="camera">%2$s</xliff:g>.</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=15]-->
<string name="ongoing_privacy_dialog_open_settings">View details</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 title when only one app is using app ops [CHAR LIMIT=NONE] -->
<string name="ongoing_privacy_dialog_single_app_title">App using your <xliff:g id="types_list" example="camera( and location)">%s</xliff:g></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 title when multiple apps is using app ops [CHAR LIMIT=NONE] -->
<string name="ongoing_privacy_dialog_multiple_apps_title">Apps using your <xliff:g id="types_list" example="camera( and location)">%s</xliff:g></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>
<!-- Separator for types. Include spaces before and after if needed [CHAR LIMIT=10] -->
<string name="ongoing_privacy_dialog_separator">,\u0020</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>
<!-- 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=12]-->
<!-- Text for camera app op [CHAR LIMIT=20]-->
<string name="privacy_type_camera">camera</string>
<!-- Text for location app op [CHAR LIMIT=12]-->
<!-- Text for location app op [CHAR LIMIT=20]-->
<string name="privacy_type_location">location</string>
<!-- Text for microphone app op [CHAR LIMIT=12]-->
<!-- Text for microphone app op [CHAR LIMIT=20]-->
<string name="privacy_type_microphone">microphone</string>
<!-- Text for indicating extra apps using app ops [CHAR LIMIT=NONE] -->
<plurals name="ongoing_privacy_dialog_overflow_text">
<item quantity="one"><xliff:g id="num_apps" example="1">%d</xliff:g> other app</item>
<item quantity="other"><xliff:g id="num_apps" example="3">%d</xliff:g> other app</item>
</plurals>
</resources>

View File

@@ -253,6 +253,18 @@
<item name="android:textSize">@dimen/qs_carrier_info_text_size</item>
</style>
<style name="TextAppearance.AppOpsDialog" />
<style name="TextAppearance.AppOpsDialog.Title">
<item name="android:textSize">@dimen/ongoing_appops_dialog_title_size</item>
<item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
</style>
<style name="TextAppearance.AppOpsDialog.Item">
<item name="android:textSize">@dimen/ongoing_appops_dialog_item_size</item>
<item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
</style>
<style name="BaseBrightnessDialogContainer" parent="@style/Theme.SystemUI">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>

View File

@@ -15,7 +15,6 @@
package com.android.systemui.privacy
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.ImageView
@@ -30,7 +29,13 @@ class OngoingPrivacyChip @JvmOverloads constructor(
defStyleRes: Int = 0
) : LinearLayout(context, attrs, defStyleAttrs, defStyleRes) {
private lateinit var appName: TextView
private val iconMargin =
context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_margin)
private val iconSize =
context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_size)
val iconColor = context.resources.getColor(
R.color.status_bar_clock_color, context.theme)
private lateinit var text: TextView
private lateinit var iconsContainer: LinearLayout
var builder = PrivacyDialogBuilder(context, emptyList<PrivacyItem>())
var privacyList = emptyList<PrivacyItem>()
@@ -43,7 +48,7 @@ class OngoingPrivacyChip @JvmOverloads constructor(
override fun onFinishInflate() {
super.onFinishInflate()
appName = findViewById(R.id.app_name)
text = findViewById(R.id.text_container)
iconsContainer = findViewById(R.id.icons_container)
}
@@ -53,39 +58,52 @@ class OngoingPrivacyChip @JvmOverloads constructor(
iconsContainer.removeAllViews()
dialogBuilder.generateIcons().forEach {
it.mutate()
it.setTint(Color.WHITE)
iconsContainer.addView(ImageView(context).apply {
it.setTint(iconColor)
val image = ImageView(context).apply {
setImageDrawable(it)
maxHeight = this@OngoingPrivacyChip.height
})
scaleType = ImageView.ScaleType.CENTER_INSIDE
}
iconsContainer.addView(image, iconSize, iconSize)
val lp = image.layoutParams as MarginLayoutParams
lp.marginStart = iconMargin
image.layoutParams = lp
}
}
if (privacyList.isEmpty()) {
return
} else {
if (!privacyList.isEmpty()) {
generateContentDescription()
setIcons(builder, iconsContainer)
appName.visibility = GONE
builder.app?.let {
appName.apply {
setText(it.applicationName)
setTextColor(Color.WHITE)
visibility = VISIBLE
text.visibility = if (builder.types.size == 1) VISIBLE else GONE
if (builder.types.size == 1) {
if (builder.app != null) {
text.setText(builder.app?.applicationName)
} else {
text.text = context.getString(R.string.ongoing_privacy_chip_multiple_apps,
builder.appsAndTypes.size)
}
}
} else {
text.visibility = GONE
iconsContainer.removeAllViews()
}
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 {
val typesText = builder.joinTypes()
if (builder.types.size > 1) {
contentDescription = context.getString(
R.string.ongoing_privacy_chip_content_multiple_apps, typesText)
} else {
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_single_op,
builder.appsAndTypes.size, typesText)
}
}
}
}

View File

@@ -18,10 +18,10 @@ import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.graphics.drawable.Drawable
import android.content.Intent
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
@@ -34,29 +34,25 @@ class OngoingPrivacyDialog constructor(
val dialogBuilder: PrivacyDialogBuilder
) {
val iconHeight = context.resources.getDimensionPixelSize(
val iconSize = 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)
companion object {
private const val MAX_ITEMS = 10
}
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,
val builder = AlertDialog.Builder(context).apply {
setNegativeButton(R.string.ongoing_privacy_dialog_cancel, null)
setPositiveButton(R.string.ongoing_privacy_dialog_open_settings,
object : DialogInterface.OnClickListener {
val intent = context.packageManager
.getLaunchIntentForPackage(dialogBuilder.app.packageName)
val intent = Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE)
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()
@@ -66,44 +62,67 @@ class OngoingPrivacyDialog constructor(
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
val title = contentView.findViewById(R.id.title) as TextView
val appsList = contentView.findViewById(R.id.items_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)
title.setText(dialogBuilder.getDialogTitle())
val numItems = dialogBuilder.appsAndTypes.size
for (i in 0..(numItems - 1)) {
if (i >= MAX_ITEMS) break
val item = dialogBuilder.appsAndTypes[i]
addAppItem(appsList, item.first, item.second, dialogBuilder.types.size > 1)
}
if (numItems > MAX_ITEMS) {
val overflow = contentView.findViewById(R.id.overflow) as LinearLayout
overflow.visibility = View.VISIBLE
val overflowText = overflow.findViewById(R.id.app_name) as TextView
overflowText.text = context.resources.getQuantityString(
R.plurals.ongoing_privacy_dialog_overflow_text,
numItems - MAX_ITEMS,
numItems - MAX_ITEMS
)
val overflowPlus = overflow.findViewById(R.id.app_icon) as ImageView
overflowPlus.apply {
imageTintList = ColorStateList.valueOf(iconColor)
setImageDrawable(context.getDrawable(R.drawable.plus))
}
}
return contentView
}
private fun addIcons(dialogBuilder: PrivacyDialogBuilder, iconsContainer: LinearLayout) {
private fun addAppItem(
itemList: LinearLayout,
app: PrivacyApplication,
types: List<PrivacyType>,
showIcons: Boolean = true
) {
val layoutInflater = LayoutInflater.from(context)
val item = layoutInflater.inflate(R.layout.ongoing_privacy_dialog_item, itemList, false)
val appIcon = item.findViewById(R.id.app_icon) as ImageView
val appName = item.findViewById(R.id.app_name) as TextView
val icons = item.findViewById(R.id.icons) as 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
app.icon?.let {
appIcon.setImageDrawable(it)
}
appName.text = app.applicationName
if (showIcons) {
dialogBuilder.generateIconsForApp(types).forEach {
it.setBounds(0, 0, iconSize, iconSize)
val image = ImageView(context).apply {
imageTintList = ColorStateList.valueOf(iconColor)
setImageDrawable(it)
}
icons.addView(image, iconSize, LinearLayout.LayoutParams.WRAP_CONTENT)
}
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) }
icons.visibility = View.VISIBLE
} else {
icons.visibility = View.GONE
}
itemList.addView(item)
}
}

View File

@@ -15,59 +15,53 @@
package com.android.systemui.privacy
import android.content.Context
import android.graphics.drawable.Drawable
import com.android.systemui.R
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 appsAndTypes: List<Pair<PrivacyApplication, List<PrivacyType>>>
val types: List<PrivacyType>
val app: PrivacyApplication?
private val separator = context.getString(R.string.ongoing_privacy_dialog_separator)
private val lastSeparator = context.getString(R.string.ongoing_privacy_dialog_last_separator)
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
appsAndTypes = itemsList.groupBy({ it.application }, { it.privacyType })
.toList()
.sortedWith(compareBy({ -it.second.size }, { it.first }))
types = itemsList.map { it.privacyType }.distinct().sorted()
val singleApp = appsAndTypes.size == 1
app = if (singleApp) appsAndTypes[0].first 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))
}
fun generateIconsForApp(types: List<PrivacyType>): List<Drawable> {
return types.sorted().map { it.getIcon(context) }
}
fun generateIcons() = types.map { it.getIcon(context) }
private fun <T> List<T>.joinWithAnd(): StringBuilder {
return subList(0, size - 1).joinTo(StringBuilder(), separator = separator).apply {
append(lastSeparator)
append(this@joinWithAnd.last())
}
}
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 joinTypes(): String {
return when (types.size) {
0 -> ""
1 -> types[0].getName(context)
else -> types.map { it.getName(context) }.joinWithAnd().toString()
}
}
fun generateText(now: Long): List<String> {
if (app == null || itemsByType.keys.size == 1) {
return itemsByType.keys.map { buildTextForItem(it, now) }
fun getDialogTitle(): String {
if (app != null) {
return context.getString(R.string.ongoing_privacy_dialog_single_app_title, joinTypes())
} else {
return buildTextForApp(itemsByType.keys)
return context.getString(R.string.ongoing_privacy_dialog_multiple_apps_title,
joinTypes())
}
}
fun generateTypesText() = itemsByType.keys.map { it.getName(context) }.sorted().joinToString()
fun generateIcons() = itemsByType.keys.map { it.getIcon(context) }
}

View File

@@ -29,16 +29,21 @@ enum class PrivacyType(val nameId: Int, val iconId: Int) {
fun getName(context: Context) = context.resources.getString(nameId)
fun getIcon(context: Context) = context.resources.getDrawable(iconId, null)
fun getIcon(context: Context) = context.resources.getDrawable(iconId, context.theme)
}
data class PrivacyItem(
val privacyType: PrivacyType,
val application: PrivacyApplication,
val timeStarted: Long
val application: PrivacyApplication
)
data class PrivacyApplication(val packageName: String, val context: Context) {
data class PrivacyApplication(val packageName: String, val context: Context)
: Comparable<PrivacyApplication> {
override fun compareTo(other: PrivacyApplication): Int {
return applicationName.compareTo(other.applicationName)
}
var icon: Drawable? = null
var applicationName: String

View File

@@ -95,7 +95,7 @@ class PrivacyItemController(val context: Context, val callback: Callback) {
else -> return null
}
val app = PrivacyApplication(appOpItem.packageName, context)
return PrivacyItem(type, app, appOpItem.timeStarted)
return PrivacyItem(type, app)
}
// Used by containing class to get notified of changes

View File

@@ -325,15 +325,10 @@ 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
public void onRtlPropertiesChanged(int layoutDirection) {
super.onRtlPropertiesChanged(layoutDirection);
@@ -378,6 +373,15 @@ public class QuickStatusBarHeader extends RelativeLayout implements
setLayoutParams(lp);
if (mPrivacyChip != null) {
MarginLayoutParams lm = (MarginLayoutParams) mPrivacyChip.getLayoutParams();
int sideMargins = lm.leftMargin;
int topBottomMargins = resources.getDimensionPixelSize(
R.dimen.ongoing_appops_top_chip_margin);
lm.setMargins(sideMargins, topBottomMargins, sideMargins, topBottomMargins);
mPrivacyChip.setLayoutParams(lm);
}
updateStatusIconAlphaAnimator();
updateHeaderTextContainerAlphaAnimator();
}
@@ -729,7 +733,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
public void setMargins(int sideMargins) {
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
if (v == mSystemIconsView || v == mQuickQsStatusIcons || v == mHeaderQsPanel) {
if (v == mSystemIconsView || v == mQuickQsStatusIcons || v == mHeaderQsPanel
|| v == mPrivacyChip) {
continue;
}
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) v.getLayoutParams();

View File

@@ -27,55 +27,28 @@ import org.junit.runner.RunWith
@SmallTest
class PrivacyDialogBuilderTest : SysuiTestCase() {
companion object {
val MILLIS_IN_MINUTE: Long = 1000 * 60
val NOW = 4 * MILLIS_IN_MINUTE
}
@Test
fun testGenerateText_multipleApps() {
fun testGenerateAppsList() {
val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
"Bar", context), 2 * MILLIS_IN_MINUTE)
"Bar", context))
val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
"Bar", context), 3 * MILLIS_IN_MINUTE)
"Bar", context))
val foo0 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
"Foo", context), 0)
"Foo", context))
val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
"Baz", context), 1 * MILLIS_IN_MINUTE)
"Baz", context))
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])
val list = textBuilder.appsAndTypes
assertEquals(3, list.size)
val appsList = list.map { it.first }
val typesList = list.map { it.second }
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_CAMERA), typesList[2])
}
}