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:
Fabian Kozynski
2018-10-12 15:33:41 -04:00
parent 07e8416f8f
commit 1263824ae5
13 changed files with 693 additions and 0 deletions

View 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>

View 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>

View File

@@ -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>

View 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"
/>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
}
}

View File

@@ -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) }
}
}
}

View File

@@ -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) }
}

View File

@@ -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
}
}
}

View File

@@ -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();
});
}
}

View File

@@ -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])
}
}