diff --git a/packages/SystemUI/res/drawable/privacy_chip_bg.xml b/packages/SystemUI/res/drawable/privacy_chip_bg.xml
new file mode 100644
index 0000000000000..8247c27ff8504
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_chip_bg.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
new file mode 100644
index 0000000000000..5e952e3c44137
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml b/packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml
new file mode 100644
index 0000000000000..b5e24a04f85e6
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_dialog_content.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_text_item.xml b/packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
new file mode 100644
index 0000000000000..5595b130e0417
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_text_item.xml
@@ -0,0 +1,24 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
index 680112c73c0d6..007070e3ffba8 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
@@ -46,6 +46,8 @@
android:layout_weight="1"
android:gravity="center_vertical|center_horizontal" />
+
+
12dp
+
+
+ 48dp
+
+ 15dp
+
+ 24dp
+
+ 12dp
+
+ 6dp
+
+ 4dp
+
+ 12dp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d67841213c7e3..7d09c0079ae85 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2238,4 +2238,39 @@
app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
Dump SysUI Heap
+
+ %1$s is using your %2$s.
+
+
+ Applications are using your %s.
+
+
+ Open app
+
+
+ Cancel
+
+
+ Okay
+
+
+ Settings
+
+
+ %1$s is using your %2$s for the last %3$d min
+
+
+ %1$s are using your %2$s
+
+
+ %1$s is using your %2$s
+
+
+ camera
+
+
+ location
+
+
+ microphone
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
new file mode 100644
index 0000000000000..3953139d43fdb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.app.ActivityManager
+import android.app.AppOpsManager
+import android.content.Context
+import android.graphics.Color
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.AttributeSet
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.Dependency
+import com.android.systemui.R
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+
+class OngoingPrivacyChip @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttrs: Int = 0,
+ defStyleRes: Int = 0
+) : LinearLayout(context, attrs, defStyleAttrs, defStyleRes) {
+
+ companion object {
+ val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
+ AppOpsManager.OP_RECORD_AUDIO,
+ AppOpsManager.OP_COARSE_LOCATION,
+ AppOpsManager.OP_FINE_LOCATION)
+ }
+
+ private lateinit var appName: TextView
+ private lateinit var iconsContainer: LinearLayout
+ private var privacyList = emptyList()
+ private val appOpsController = Dependency.get(AppOpsController::class.java)
+ private val userManager = context.getSystemService(UserManager::class.java)
+ private val currentUser = ActivityManager.getCurrentUser()
+ private val currentUserIds = userManager.getProfiles(currentUser).map { it.id }
+ private var listening = false
+
+ var builder = PrivacyDialogBuilder(context, privacyList)
+
+ private val callback = object : AppOpsController.Callback {
+ override fun onActiveStateChanged(
+ code: Int,
+ uid: Int,
+ packageName: String,
+ active: Boolean
+ ) {
+ val userId = UserHandle.getUserId(uid)
+ if (userId in currentUserIds) {
+ updatePrivacyList()
+ }
+ }
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ appName = findViewById(R.id.app_name)
+ iconsContainer = findViewById(R.id.icons_container)
+ }
+
+ fun setListening(listen: Boolean) {
+ if (listening == listen) return
+ listening = listen
+ if (listening) {
+ appOpsController.addCallback(OPS, callback)
+ updatePrivacyList()
+ } else {
+ appOpsController.removeCallback(OPS, callback)
+ }
+ }
+
+ private fun updatePrivacyList() {
+ privacyList = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
+ .mapNotNull { toPrivacyItem(it) }
+ builder = PrivacyDialogBuilder(context, privacyList)
+ updateView()
+ }
+
+ private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
+ val type: PrivacyType = when (appOpItem.code) {
+ AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
+ AppOpsManager.OP_COARSE_LOCATION -> PrivacyType.TYPE_LOCATION
+ AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
+ AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
+ else -> return null
+ }
+ val app = PrivacyApplication(appOpItem.packageName, context)
+ return PrivacyItem(type, app, appOpItem.timeStarted)
+ }
+
+ // Should only be called if the builder icons or app changed
+ private fun updateView() {
+ fun setIcons(dialogBuilder: PrivacyDialogBuilder, iconsContainer: ViewGroup) {
+ iconsContainer.removeAllViews()
+ dialogBuilder.generateIcons().forEach {
+ it.mutate()
+ it.setTint(Color.WHITE)
+ iconsContainer.addView(ImageView(context).apply {
+ setImageDrawable(it)
+ maxHeight = this@OngoingPrivacyChip.height
+ })
+ }
+ }
+
+ if (privacyList.isEmpty()) {
+ visibility = GONE
+ return
+ } else {
+ generateContentDescription()
+ visibility = VISIBLE
+ setIcons(builder, iconsContainer)
+ appName.visibility = GONE
+ builder.app?.let {
+ appName.apply {
+ setText(it.applicationName)
+ setTextColor(Color.WHITE)
+ visibility = VISIBLE
+ }
+ }
+ }
+ requestLayout()
+ }
+
+ private fun generateContentDescription() {
+ val typesText = builder.generateTypesText()
+ if (builder.app != null) {
+ contentDescription = context.getString(R.string.ongoing_privacy_chip_content_single_app,
+ builder.app?.applicationName, typesText)
+ } else {
+ contentDescription = context.getString(
+ R.string.ongoing_privacy_chip_content_multiple_apps, typesText)
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
new file mode 100644
index 0000000000000..1d0e16ed33343
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyDialog.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.Dependency
+import com.android.systemui.R
+import com.android.systemui.plugins.ActivityStarter
+
+class OngoingPrivacyDialog constructor(
+ val context: Context,
+ val dialogBuilder: PrivacyDialogBuilder
+) {
+
+ val iconHeight = context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_appops_dialog_icon_height)
+ val textMargin = context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_appops_dialog_text_margin)
+ val iconColor = context.resources.getColor(
+ com.android.internal.R.color.text_color_primary, context.theme)
+
+ fun createDialog(): Dialog {
+ val builder = AlertDialog.Builder(context)
+ .setNeutralButton(R.string.ongoing_privacy_dialog_open_settings, null)
+ if (dialogBuilder.app != null) {
+ builder.setPositiveButton(R.string.ongoing_privacy_dialog_open_app,
+ object : DialogInterface.OnClickListener {
+ val intent = context.packageManager
+ .getLaunchIntentForPackage(dialogBuilder.app.packageName)
+
+ override fun onClick(dialog: DialogInterface?, which: Int) {
+ Dependency.get(ActivityStarter::class.java).startActivity(intent, false)
+ }
+ })
+ builder.setNegativeButton(R.string.ongoing_privacy_dialog_cancel, null)
+ } else {
+ builder.setPositiveButton(R.string.ongoing_privacy_dialog_okay, null)
+ }
+ builder.setView(getContentView())
+ return builder.create()
+ }
+
+ fun getContentView(): View {
+ val layoutInflater = LayoutInflater.from(context)
+ val contentView = layoutInflater.inflate(R.layout.ongoing_privacy_dialog_content, null)
+
+ val iconsContainer = contentView.findViewById(R.id.icons_container) as LinearLayout
+ val textContainer = contentView.findViewById(R.id.text_container) as LinearLayout
+
+ addIcons(dialogBuilder, iconsContainer)
+ val lm = ViewGroup.MarginLayoutParams(
+ ViewGroup.MarginLayoutParams.WRAP_CONTENT,
+ ViewGroup.MarginLayoutParams.WRAP_CONTENT)
+ lm.topMargin = textMargin
+ val now = System.currentTimeMillis()
+ dialogBuilder.generateText(now).forEach {
+ val text = layoutInflater.inflate(R.layout.ongoing_privacy_text_item, null) as TextView
+ text.setText(it)
+ textContainer.addView(text, lm)
+ }
+ return contentView
+ }
+
+ private fun addIcons(dialogBuilder: PrivacyDialogBuilder, iconsContainer: LinearLayout) {
+
+ fun LinearLayout.addIcon(icon: Drawable) {
+ val image = ImageView(context).apply {
+ setImageDrawable(icon.apply {
+ setBounds(0, 0, iconHeight, iconHeight)
+ maxHeight = this@addIcon.height
+ })
+ adjustViewBounds = true
+ }
+ addView(image, LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.MATCH_PARENT)
+ }
+
+ dialogBuilder.generateIcons().forEach {
+ it.mutate()
+ it.setTint(iconColor)
+ iconsContainer.addIcon(it)
+ }
+ dialogBuilder.app.let {
+ it?.icon?.let { iconsContainer.addIcon(it) }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt
new file mode 100644
index 0000000000000..2f86f78d7669b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogBuilder.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.content.Context
+import com.android.systemui.R
+import java.lang.IllegalStateException
+import java.lang.Math.max
+
+class PrivacyDialogBuilder(val context: Context, itemsList: List) {
+ companion object {
+ val MILLIS_IN_MINUTE: Long = 1000 * 60
+ }
+
+ private val itemsByType: Map>
+ 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())
+ 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): List {
+ 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 {
+ if (app == null || itemsByType.keys.size == 1) {
+ return itemsByType.keys.map { buildTextForItem(it, now) }
+ } else {
+ return buildTextForApp(itemsByType.keys)
+ }
+ }
+
+ fun generateTypesText() = itemsByType.keys.map { it.getName(context) }.sorted().joinToString()
+
+ fun generateIcons() = itemsByType.keys.map { it.getIcon(context) }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
new file mode 100644
index 0000000000000..f4099021a0bd9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import com.android.systemui.R
+
+typealias Privacy = PrivacyType
+
+enum class PrivacyType(val nameId: Int, val iconId: Int) {
+ TYPE_CAMERA(R.string.privacy_type_camera, com.android.internal.R.drawable.ic_camera),
+ TYPE_LOCATION(R.string.privacy_type_location, R.drawable.stat_sys_location),
+ TYPE_MICROPHONE(R.string.privacy_type_microphone, R.drawable.ic_mic_26dp);
+
+ fun getName(context: Context) = context.resources.getString(nameId)
+
+ fun getIcon(context: Context) = context.resources.getDrawable(iconId, null)
+}
+
+data class PrivacyItem(
+ val privacyType: PrivacyType,
+ val application: PrivacyApplication,
+ val timeStarted: Long
+)
+
+data class PrivacyApplication(val packageName: String, val context: Context) {
+ var icon: Drawable? = null
+ var applicationName: String
+
+ init {
+ try {
+ val app: ApplicationInfo = context.packageManager
+ .getApplicationInfo(packageName, 0)
+ icon = context.packageManager.getApplicationIcon(app)
+ applicationName = context.packageManager.getApplicationLabel(app) as String
+ } catch (e: PackageManager.NameNotFoundException) {
+ applicationName = packageName
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 326df498c9847..3ee6195858d62 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter;
import android.annotation.ColorInt;
import android.app.ActivityManager;
import android.app.AlarmManager;
+import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -31,6 +32,7 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Handler;
+import android.os.Looper;
import android.provider.AlarmClock;
import android.service.notification.ZenModeConfig;
import android.text.format.DateUtils;
@@ -39,6 +41,7 @@ import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.WindowInsets;
+import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
@@ -52,11 +55,14 @@ import com.android.systemui.Dependency;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.privacy.OngoingPrivacyChip;
+import com.android.systemui.privacy.OngoingPrivacyDialog;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
import com.android.systemui.statusbar.phone.StatusIconContainer;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.DarkIconDispatcher;
import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
@@ -118,6 +124,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
private BatteryMeterView mBatteryMeterView;
private Clock mClockView;
private DateView mDateView;
+ private OngoingPrivacyChip mPrivacyChip;
private NextAlarmController mAlarmController;
private ZenModeController mZenController;
@@ -185,6 +192,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mClockView = findViewById(R.id.clock);
mClockView.setOnClickListener(this);
mDateView = findViewById(R.id.date);
+ mPrivacyChip = findViewById(R.id.privacy_chip);
+ mPrivacyChip.setOnClickListener(this);
}
private void updateStatusText() {
@@ -263,6 +272,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
mBatteryMeterView.useWallpaperTextColor(shouldUseWallpaperTextColor);
mClockView.useWallpaperTextColor(shouldUseWallpaperTextColor);
+
+ MarginLayoutParams lm = (MarginLayoutParams) mPrivacyChip.getLayoutParams();
+ int sideMargins = lm.leftMargin;
+ int topBottomMargins = (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
+ ? 0 : sideMargins;
+ lm.setMargins(sideMargins, topBottomMargins, sideMargins, topBottomMargins);
+ mPrivacyChip.setLayoutParams(lm);
}
@Override
@@ -421,6 +437,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
return;
}
mHeaderQsPanel.setListening(listening);
+ mPrivacyChip.setListening(listening);
mListening = listening;
if (listening) {
@@ -443,6 +460,19 @@ public class QuickStatusBarHeader extends RelativeLayout implements
} else if (v == mBatteryMeterView) {
Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(new Intent(
Intent.ACTION_POWER_USAGE_SUMMARY),0);
+ } else if (v == mPrivacyChip) {
+ Handler mUiHandler = new Handler(Looper.getMainLooper());
+ mUiHandler.post(() -> {
+ Dialog mDialog = new OngoingPrivacyDialog(mContext,
+ mPrivacyChip.getBuilder()).createDialog();
+ mDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
+ SystemUIDialog.setShowForAllUsers(mDialog, true);
+ SystemUIDialog.registerDismissListener(mDialog);
+ SystemUIDialog.setWindowOnTop(mDialog);
+ mUiHandler.post(() -> mDialog.show());
+ mHost.collapsePanels();
+ });
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt
new file mode 100644
index 0000000000000..7204d310a76d1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogBuilderTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PrivacyDialogBuilderTest : SysuiTestCase() {
+
+ companion object {
+ val MILLIS_IN_MINUTE: Long = 1000 * 60
+ val NOW = 4 * MILLIS_IN_MINUTE
+ }
+
+ @Test
+ fun testGenerateText_multipleApps() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", context), 2 * MILLIS_IN_MINUTE)
+ val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
+ "Bar", context), 3 * MILLIS_IN_MINUTE)
+ val foo0 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Foo", context), 0)
+ val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Baz", context), 1 * MILLIS_IN_MINUTE)
+
+ val items = listOf(bar2, foo0, baz1, bar3)
+
+ val textBuilder = PrivacyDialogBuilder(context, items)
+
+ val textList = textBuilder.generateText(NOW)
+ assertEquals(2, textList.size)
+ assertEquals("Bar, Foo, Baz are using your camera", textList[0])
+ assertEquals("Bar is using your location for the last 1 min", textList[1])
+ }
+
+ @Test
+ fun testGenerateText_singleApp() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", context), 0)
+ val bar1 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
+ "Bar", context), 0)
+
+ val items = listOf(bar2, bar1)
+
+ val textBuilder = PrivacyDialogBuilder(context, items)
+ val textList = textBuilder.generateText(NOW)
+ assertEquals(1, textList.size)
+ assertEquals("Bar is using your camera, location", textList[0])
+ }
+
+ @Test
+ fun testGenerateText_singleApp_singleType() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", context), 2 * MILLIS_IN_MINUTE)
+ val items = listOf(bar2)
+ val textBuilder = PrivacyDialogBuilder(context, items)
+ val textList = textBuilder.generateText(NOW)
+ assertEquals(1, textList.size)
+ assertEquals("Bar is using your camera for the last 2 min", textList[0])
+ }
+}
\ No newline at end of file