Add notification visibility settings

This commit is contained in:
ajp_anton
2026-08-29 13:21:07 +00:00
parent 4fcbffd4a5
commit 47fbda8a5e
10 changed files with 584 additions and 1 deletions
@@ -36,4 +36,19 @@ class VisibilityPolicyStoreTest {
assertFalse(raw.contains("example.app"))
assertTrue(File(context.filesDir, "visibility-policy.bin").length() > 0)
}
@Test fun updateAndPackageCleanupKeepOnlyInstalledPolicies() {
val store = VisibilityPolicyStore(context)
store.update { policy ->
policy.copy(apps = listOf(
AppVisibilityPolicy("installed.app", setOf(NotificationSurface.AOD)),
AppVisibilityPolicy("removed.app", setOf(NotificationSurface.UNLOCKED_STATUSBAR)),
))
}
store.removeUninstalledPackages(setOf("installed.app"))
assertEquals(listOf("installed.app"), store.load().apps.map { it.packageName })
assertEquals(2, store.load().generation)
}
}
@@ -0,0 +1,251 @@
package se.ajpanton.notificationsmaster
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.switchmaterial.SwitchMaterial
import se.ajpanton.notificationsmaster.databinding.FragmentAppVisibilityBinding
import se.ajpanton.notificationsmaster.visibility.AppVisibilityPolicy
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
import se.ajpanton.notificationsmaster.visibility.VisibilityExceptionRule
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
import java.util.Collections
class AppVisibilityFragment : Fragment(R.layout.fragment_app_visibility) {
private var binding: FragmentAppVisibilityBinding? = null
private lateinit var store: VisibilityPolicyStore
private lateinit var packageName: String
private val adapter = ExceptionAdapter(::editException, ::deleteException)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentAppVisibilityBinding.bind(view)
store = VisibilityPolicyStore(requireContext())
packageName = requireArguments().getString(ARG_PACKAGE)!!
binding!!.appName.text = InstalledApps.name(requireContext(), packageName)
binding!!.packageName.text = packageName
binding!!.exceptions.layoutManager = LinearLayoutManager(requireContext())
binding!!.exceptions.adapter = adapter
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
override fun onMove(list: RecyclerView, from: RecyclerView.ViewHolder, to: RecyclerView.ViewHolder): Boolean {
adapter.move(from.bindingAdapterPosition, to.bindingAdapterPosition)
saveApp { copy(exceptions = adapter.rules()) }
return true
}
override fun onSwiped(holder: RecyclerView.ViewHolder, direction: Int) = Unit
}).attachToRecyclerView(binding!!.exceptions)
binding!!.addException.setOnClickListener { showExceptionDialog(null) }
bind()
}
override fun onResume() {
super.onResume()
activity?.title = getString(R.string.page_notification_visibility)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private fun bind() {
val policy = appPolicy()
bindSurface(binding!!.hideUnlocked, NotificationSurface.UNLOCKED_STATUSBAR, policy)
bindSurface(binding!!.hideLockscreen, NotificationSurface.LOCKSCREEN_COLLAPSED, policy)
bindSurface(binding!!.hideAod, NotificationSurface.AOD, policy)
adapter.submit(policy.exceptions)
}
private fun bindSurface(toggle: SwitchMaterial, surface: NotificationSurface, policy: AppVisibilityPolicy) {
toggle.setOnCheckedChangeListener(null)
toggle.isChecked = surface in policy.blockedSurfaces
toggle.setOnCheckedChangeListener { _, hidden ->
saveApp {
copy(blockedSurfaces = blockedSurfaces.toMutableSet().apply {
if (hidden) add(surface) else remove(surface)
})
}
}
}
private fun appPolicy() = store.load().apps.firstOrNull { it.packageName == packageName }
?: AppVisibilityPolicy(packageName)
private fun saveApp(change: AppVisibilityPolicy.() -> AppVisibilityPolicy) {
store.update { policy ->
val updated = (policy.apps.firstOrNull { it.packageName == packageName }
?: AppVisibilityPolicy(packageName)).change()
val apps = policy.apps.filterNot { it.packageName == packageName }.toMutableList()
if (updated.blockedSurfaces.isNotEmpty() || updated.exceptions.isNotEmpty()) apps += updated
policy.copy(apps = apps.sortedBy { it.packageName })
}
}
private fun editException(position: Int) = showExceptionDialog(position)
private fun deleteException(position: Int) {
saveApp { copy(exceptions = exceptions.toMutableList().apply { removeAt(position) }) }
bind()
}
private fun showExceptionDialog(position: Int?) {
val existing = position?.let { appPolicy().exceptions[it] }
val content = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), 0, dp(24), 0)
}
val pattern = EditText(requireContext()).apply {
hint = "Regular expression"
setText(existing?.pattern)
maxLines = 4
}
content.addView(pattern)
val toggles = listOf(
NotificationSurface.UNLOCKED_STATUSBAR to "Hide in unlocked status bar",
NotificationSurface.LOCKSCREEN_COLLAPSED to "Hide on collapsed lockscreen",
NotificationSurface.AOD to "Hide on always-on display",
).associate { (surface, label) ->
surface to SwitchMaterial(requireContext()).apply {
text = label
isChecked = surface in existing?.blockedSurfaces.orEmpty()
content.addView(this)
}
}
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(if (existing == null) "Add text exception" else "Edit text exception")
.setView(content)
.setNegativeButton("Cancel", null)
.setPositiveButton("Save", null)
.create()
dialog.setOnShowListener {
dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val rule = runCatching {
VisibilityExceptionRule(
pattern.text.toString(),
toggles.filterValues { it.isChecked }.keys,
).also { require(it.hasValidPattern()) }
}.getOrElse {
pattern.error = "Enter a valid, non-empty regular expression"
return@setOnClickListener
}
saveApp {
copy(exceptions = exceptions.toMutableList().apply {
if (position == null) add(rule) else set(position, rule)
})
}
bind()
dialog.dismiss()
}
}
dialog.show()
}
private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
private class ExceptionAdapter(
private val edit: (Int) -> Unit,
private val delete: (Int) -> Unit,
) : RecyclerView.Adapter<ExceptionHolder>() {
private val items = mutableListOf<VisibilityExceptionRule>()
fun submit(rules: List<VisibilityExceptionRule>) {
items.clear()
items.addAll(rules)
notifyDataSetChanged()
}
fun move(from: Int, to: Int) {
Collections.swap(items, from, to)
notifyItemMoved(from, to)
}
fun rules() = items.toList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExceptionHolder {
val row = LinearLayout(parent.context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.CENTER_VERTICAL
setPadding(0, dp(parent, 6), 0, dp(parent, 6))
isClickable = true
isFocusable = true
background = context.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
.let { values -> values.getDrawable(0).also { values.recycle() } }
}
val handle = TextView(parent.context).apply {
text = ""
textSize = 22f
gravity = android.view.Gravity.CENTER
contentDescription = "Drag to reorder"
layoutParams = LinearLayout.LayoutParams(dp(parent, 40), dp(parent, 48))
}
val textColumn = LinearLayout(parent.context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
val pattern = TextView(parent.context).apply { textSize = 15f }
val summary = TextView(parent.context).apply { textSize = 12f }
textColumn.addView(pattern)
textColumn.addView(summary)
val remove = Button(parent.context).apply {
text = "Delete"
isAllCaps = false
}
row.addView(handle)
row.addView(textColumn)
row.addView(remove)
return ExceptionHolder(row, pattern, summary, remove)
}
override fun onBindViewHolder(holder: ExceptionHolder, position: Int) {
val rule = items[position]
holder.pattern.text = rule.pattern
holder.summary.text = surfaceSummary(rule.blockedSurfaces)
holder.itemView.setOnClickListener {
holder.bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }?.let(edit)
}
holder.remove.setOnClickListener {
holder.bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }?.let(delete)
}
}
override fun getItemCount() = items.size
private fun surfaceSummary(surfaces: Set<NotificationSurface>): String {
val names = listOf(
NotificationSurface.UNLOCKED_STATUSBAR to "status bar",
NotificationSurface.LOCKSCREEN_COLLAPSED to "lockscreen",
NotificationSurface.AOD to "AOD",
).filter { it.first in surfaces }.joinToString { it.second }
return if (names.isEmpty()) "Show on all surfaces" else "Hide: $names"
}
private fun dp(parent: ViewGroup, value: Int) =
(value * parent.resources.displayMetrics.density).toInt()
}
private class ExceptionHolder(
view: View,
val pattern: TextView,
val summary: TextView,
val remove: Button,
) : RecyclerView.ViewHolder(view)
companion object {
private const val ARG_PACKAGE = "package"
private const val UP = ItemTouchHelper.UP
private const val DOWN = ItemTouchHelper.DOWN
fun newInstance(packageName: String) = AppVisibilityFragment().apply {
arguments = Bundle().apply { putString(ARG_PACKAGE, packageName) }
}
}
}
@@ -9,6 +9,7 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.RelativeLayout
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
@@ -166,6 +167,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
page == Page.PROFILES -> ProfilesFragment()
page == Page.RULES -> RulesFragment()
page == Page.APPS -> AppsFragment()
page == Page.NOTIFICATION_VISIBILITY -> NotificationVisibilityFragment()
page == Page.LOG_DISPLAY -> LogDisplayFragment()
page == Page.FILTER_LOGGING -> FilterLoggingFragment()
page == Page.FILTER_APPS -> FilterAppsFragment.newInstance()
@@ -281,6 +283,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
binding.navViewDrawer.visibility = View.VISIBLE
}
(binding.contentFrame.layoutParams as RelativeLayout.LayoutParams).also { params ->
params.removeRule(if (permanentSidebar) RelativeLayout.BELOW else RelativeLayout.ALIGN_PARENT_TOP)
params.addRule(
if (permanentSidebar) RelativeLayout.ALIGN_PARENT_TOP else RelativeLayout.BELOW,
if (permanentSidebar) RelativeLayout.TRUE else R.id.toolbar,
)
binding.contentFrame.layoutParams = params
}
updateNavigationHeaderHeight(binding.navViewDrawer)
updateNavigationHeaderHeight(binding.navViewPermanent)
applyPageTitleVisibility()
@@ -362,6 +372,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
PROFILES(R.id.nav_profiles, R.string.page_profiles),
RULES(R.id.nav_rules, R.string.page_rules),
APPS(R.id.nav_apps, R.string.page_apps),
NOTIFICATION_VISIBILITY(R.id.nav_notification_visibility, R.string.page_notification_visibility),
LOG_DISPLAY(R.id.nav_log_display, R.string.page_log_display),
FILTER_LOGGING(R.id.nav_filter_logging, R.string.page_filter_logging),
FILTER_APPS(R.id.nav_filter_apps, R.string.page_filter_apps),
@@ -7,6 +7,7 @@ import se.ajpanton.notificationsmaster.module.VisibilityPolicySync
import se.ajpanton.notificationsmaster.settings.AppFilterSettingsStore
import se.ajpanton.notificationsmaster.settings.NotificationListenerComponentController
import se.ajpanton.notificationsmaster.settings.PerAppEventSettingsStore
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
class NotificationLogApplication : Application() {
override fun onCreate() {
@@ -31,6 +32,7 @@ class NotificationLogApplication : Application() {
).mapTo(mutableSetOf()) { it.packageName }
AppFilterSettingsStore(this).removeUninstalledPackages(installedPackages)
PerAppEventSettingsStore(this).removeUninstalledPackages(installedPackages)
VisibilityPolicyStore(this).removeUninstalledPackages(installedPackages)
EncryptedNotificationLogStore(this).removeOrphanedImages()
}
}
@@ -0,0 +1,157 @@
package se.ajpanton.notificationsmaster
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import se.ajpanton.notificationsmaster.databinding.FragmentNotificationVisibilityBinding
import se.ajpanton.notificationsmaster.visibility.AppVisibilityPolicy
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
class NotificationVisibilityFragment : Fragment(R.layout.fragment_notification_visibility) {
private var binding: FragmentNotificationVisibilityBinding? = null
private lateinit var store: VisibilityPolicyStore
private val adapter = AppAdapter(::openApp)
private var apps = emptyList<InstalledApp>()
private var appPolicies = emptyMap<String, AppVisibilityPolicy>()
private var loadGeneration = 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentNotificationVisibilityBinding.bind(view)
store = VisibilityPolicyStore(requireContext())
binding!!.appList.layoutManager = LinearLayoutManager(requireContext())
binding!!.appList.adapter = adapter
binding!!.appSearch.doAfterTextChanged { showApps() }
bindEnabled()
loadApps()
}
override fun onResume() {
super.onResume()
activity?.title = getString(R.string.page_notification_visibility)
if (binding != null) {
bindEnabled()
showApps()
}
}
override fun onDestroyView() {
loadGeneration++
binding = null
super.onDestroyView()
}
private fun bindEnabled() {
val toggle = binding?.visibilityEnabled ?: return
val policy = store.load()
appPolicies = policy.apps.associateBy(AppVisibilityPolicy::packageName)
toggle.setOnCheckedChangeListener(null)
toggle.isChecked = policy.enabled
toggle.setOnCheckedChangeListener { _, enabled -> store.update { it.copy(enabled = enabled) } }
}
private fun loadApps() {
val currentBinding = binding ?: return
val generation = ++loadGeneration
val context = requireContext().applicationContext
currentBinding.appListLoading.visibility = View.VISIBLE
viewLifecycleOwner.lifecycleScope.launch {
val loaded = withContext(Dispatchers.IO) { InstalledApps.all(context) }
if (binding === currentBinding && generation == loadGeneration) {
apps = loaded
currentBinding.appListLoading.visibility = View.GONE
showApps()
}
}
}
private fun showApps() {
val currentBinding = binding ?: return
val terms = currentBinding.appSearch.text.toString().lowercase().trim()
.split(Regex("\\s+")).filter(String::isNotEmpty)
adapter.submit(apps.filter { app ->
val searchable = "${app.name}\n${app.packageName}".lowercase()
terms.all(searchable::contains)
}.map { app -> AppRow(app, appPolicies[app.packageName]) })
}
private fun openApp(app: InstalledApp) = parentFragmentManager.commit {
replace(R.id.content_frame, AppVisibilityFragment.newInstance(app.packageName))
addToBackStack(null)
}
private data class AppRow(val app: InstalledApp, val policy: AppVisibilityPolicy?)
private class AppAdapter(private val open: (InstalledApp) -> Unit) : RecyclerView.Adapter<AppHolder>() {
private var rows = emptyList<AppRow>()
fun submit(newRows: List<AppRow>) {
rows = newRows
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppHolder {
val row = LinearLayout(parent.context).apply {
orientation = LinearLayout.VERTICAL
isClickable = true
isFocusable = true
setPadding(0, dp(parent, 8), 0, dp(parent, 8))
background = context.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackground))
.let { values -> values.getDrawable(0).also { values.recycle() } }
}
val name = TextView(parent.context).apply {
textSize = 16f
setTypeface(typeface, android.graphics.Typeface.BOLD)
}
val packageName = TextView(parent.context).apply { textSize = 12f }
val summary = TextView(parent.context).apply { textSize = 13f }
row.addView(name)
row.addView(packageName)
row.addView(summary)
return AppHolder(row, name, packageName, summary)
}
override fun onBindViewHolder(holder: AppHolder, position: Int) {
val row = rows[position]
holder.name.text = row.app.name
holder.packageName.text = row.app.packageName
holder.summary.text = summary(row.policy)
holder.itemView.setOnClickListener { open(row.app) }
}
override fun getItemCount() = rows.size
private fun summary(policy: AppVisibilityPolicy?): String {
if (policy == null) return "Visible on all surfaces"
val hidden = listOf(
NotificationSurface.UNLOCKED_STATUSBAR to "status bar",
NotificationSurface.LOCKSCREEN_COLLAPSED to "lockscreen",
NotificationSurface.AOD to "AOD",
).filter { it.first in policy.blockedSurfaces }.joinToString { it.second }
val defaults = if (hidden.isEmpty()) "Visible on all surfaces" else "Hidden: $hidden"
return if (policy.exceptions.isEmpty()) defaults else "$defaults${policy.exceptions.size} text exception" +
if (policy.exceptions.size == 1) "" else "s"
}
private fun dp(parent: ViewGroup, value: Int) =
(value * parent.resources.displayMetrics.density).toInt()
}
private class AppHolder(
view: View,
val name: TextView,
val packageName: TextView,
val summary: TextView,
) : RecyclerView.ViewHolder(view)
}
@@ -58,6 +58,16 @@ class VisibilityPolicyStore(context: Context) {
return stored
}
@Synchronized
fun update(change: (VisibilityPolicy) -> VisibilityPolicy) = save(change(load()))
@Synchronized
fun removeUninstalledPackages(installedPackages: Set<String>) {
val current = load()
val retained = current.apps.filter { it.packageName in installedPackages }
if (retained.size != current.apps.size) save(current.copy(apps = retained))
}
private companion object {
const val FILE_NAME = "visibility-policy.bin"
const val MAX_CIPHER_TEXT_BYTES = 1024 * 1024
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/page_padding">
<TextView
android:id="@+id/app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textStyle="bold" />
<TextView
android:id="@+id/package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody2" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Hide by default"
android:textStyle="bold" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_unlocked"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unlocked status bar" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_lockscreen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Collapsed lockscreen" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/hide_aod"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Always-on display" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Ordered text exceptions"
android:textStyle="bold" />
<Button
android:id="@+id/add_exception"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="The first matching expression replaces the defaults above. Drag to reorder."
android:textAppearance="?attr/textAppearanceBody2" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/exceptions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingBottom="16dp" />
</LinearLayout>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/page_padding">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/visibility_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enable notification visibility rules" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Requires LSPosed. Unsupported surfaces remain unchanged."
android:textAppearance="?attr/textAppearanceBody2" />
<EditText
android:id="@+id/app_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="Filter apps"
android:inputType="text"
android:maxLines="1" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fastScrollAlwaysVisible="true"
android:fastScrollEnabled="true"
android:paddingBottom="16dp" />
<ProgressBar
android:id="@+id/app_list_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</LinearLayout>
+3
View File
@@ -21,6 +21,9 @@
<item
android:id="@+id/nav_apps"
android:title="@string/navigation_indented_apps" />
<item
android:id="@+id/nav_notification_visibility"
android:title="@string/navigation_indented_notification_visibility" />
</group>
<group android:checkableBehavior="single">
<item
+3 -1
View File
@@ -4,7 +4,7 @@
<string name="notification_listener_label">Notifications Master listener</string>
<string name="navigation_open">Open navigation</string>
<string name="navigation_close">Close navigation</string>
<string name="navigation_alerts_header">Alert control</string>
<string name="navigation_alerts_header">Notification control</string>
<string name="navigation_logging_header">Logging</string>
<string name="page_view_logs">View logs</string>
<string name="page_settings">Settings</string>
@@ -13,6 +13,7 @@
<string name="page_profiles">Profiles</string>
<string name="page_rules">Rules</string>
<string name="page_apps">Apps</string>
<string name="page_notification_visibility">Notification visibility</string>
<string name="page_filter_apps">Filter apps</string>
<string name="page_log_display">Log display</string>
<string name="page_filter_logging">Filter logging</string>
@@ -21,6 +22,7 @@
<string name="navigation_indented_profiles">&#160;&#160;&#160;&#160;Profiles</string>
<string name="navigation_indented_rules">&#160;&#160;&#160;&#160;Rules</string>
<string name="navigation_indented_apps">&#160;&#160;&#160;&#160;Apps</string>
<string name="navigation_indented_notification_visibility">&#160;&#160;&#160;&#160;Visibility</string>
<string name="navigation_indented_filter_logging">&#160;&#160;&#160;&#160;Filter logging</string>
<string name="navigation_indented_filter_apps">&#160;&#160;&#160;&#160;Filter apps</string>
</resources>