Dialog and chip for privacy showing
Creates a chip and a dialog to show the current apps using certain app ops (location, microphone, camera). Dimens are estimated. Settings button dismisses dialog for now. Test: atest && visual Bug: 117646163 Change-Id: Ida5b42acf331d6c9da06141379eadc0da5e72df2
This commit is contained in:
22
packages/SystemUI/res/drawable/privacy_chip_bg.xml
Normal file
22
packages/SystemUI/res/drawable/privacy_chip_bg.xml
Normal file
@@ -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>
|
||||
44
packages/SystemUI/res/layout/ongoing_privacy_chip.xml
Normal file
44
packages/SystemUI/res/layout/ongoing_privacy_chip.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
24
packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
Normal file
24
packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
Normal file
@@ -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"
|
||||
/>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user