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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
53
packages/SystemUI/res/layout/ongoing_privacy_dialog_item.xml
Normal file
53
packages/SystemUI/res/layout/ongoing_privacy_dialog_item.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user