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