Add built-in debug notification generator
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="se.ajpanton.notificationsmaster.permission.UPDATE_ALERT_POLICY" />
|
<uses-permission android:name="se.ajpanton.notificationsmaster.permission.UPDATE_ALERT_POLICY" />
|
||||||
|
|
||||||
<permission
|
<permission
|
||||||
|
|||||||
@@ -1,21 +1,55 @@
|
|||||||
package se.ajpanton.notificationsmaster
|
package se.ajpanton.notificationsmaster
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
|
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
|
||||||
import se.ajpanton.notificationsmaster.capture.NotificationCaptureService
|
import se.ajpanton.notificationsmaster.capture.NotificationCaptureService
|
||||||
import se.ajpanton.notificationsmaster.databinding.FragmentAlertStatusBinding
|
import se.ajpanton.notificationsmaster.databinding.FragmentAlertStatusBinding
|
||||||
|
import se.ajpanton.notificationsmaster.debug.DebugNotifications
|
||||||
|
|
||||||
class AlertStatusFragment : Fragment(R.layout.fragment_alert_status) {
|
class AlertStatusFragment : Fragment(R.layout.fragment_alert_status) {
|
||||||
private var binding: FragmentAlertStatusBinding? = null
|
private var binding: FragmentAlertStatusBinding? = null
|
||||||
|
private var pendingCount: Int? = null
|
||||||
|
private var pendingCycle = false
|
||||||
|
private var updatingCycle = false
|
||||||
|
private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
val context = context ?: return@registerForActivityResult
|
||||||
|
if (granted) {
|
||||||
|
pendingCount?.let { DebugNotifications.setCount(context, it) }
|
||||||
|
if (pendingCycle) DebugNotifications.setCycling(context, true)
|
||||||
|
refreshDebugControls()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, R.string.debug_notification_permission_required, Toast.LENGTH_SHORT).show()
|
||||||
|
refreshDebugControls()
|
||||||
|
}
|
||||||
|
pendingCount = null
|
||||||
|
pendingCycle = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
binding = FragmentAlertStatusBinding.bind(view)
|
binding = FragmentAlertStatusBinding.bind(view)
|
||||||
binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) }
|
binding!!.notificationAccess.setOnClickListener { startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) }
|
||||||
|
binding!!.debugMinus10.setOnClickListener { changeDebugCount(-10) }
|
||||||
|
binding!!.debugMinus1.setOnClickListener { changeDebugCount(-1) }
|
||||||
|
binding!!.debugPlus1.setOnClickListener { changeDebugCount(1) }
|
||||||
|
binding!!.debugPlus10.setOnClickListener { changeDebugCount(10) }
|
||||||
|
binding!!.debugReset.setOnClickListener { setDebugCount(0) }
|
||||||
|
binding!!.debugCycle.setOnCheckedChangeListener { _, enabled ->
|
||||||
|
if (updatingCycle) return@setOnCheckedChangeListener
|
||||||
|
if (enabled && !DebugNotifications.hasPermission(requireContext())) {
|
||||||
|
pendingCycle = true
|
||||||
|
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
} else {
|
||||||
|
DebugNotifications.setCycling(requireContext(), enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -32,6 +66,31 @@ class AlertStatusFragment : Fragment(R.layout.fragment_alert_status) {
|
|||||||
append(if (config.rules.count { it.enabled } == 1) "." else "s.")
|
append(if (config.rules.count { it.enabled } == 1) "." else "s.")
|
||||||
}
|
}
|
||||||
binding?.notificationAccess?.text = if (access) "Notification access enabled" else "Enable notification access"
|
binding?.notificationAccess?.text = if (access) "Notification access enabled" else "Enable notification access"
|
||||||
|
DebugNotifications.apply(context)
|
||||||
|
refreshDebugControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeDebugCount(delta: Int) = setDebugCount(DebugNotifications.count(requireContext()) + delta)
|
||||||
|
|
||||||
|
private fun setDebugCount(count: Int) {
|
||||||
|
val desired = count.coerceIn(0, DebugNotifications.MAX_COUNT)
|
||||||
|
if (desired > 0 && !DebugNotifications.hasPermission(requireContext())) {
|
||||||
|
pendingCount = desired
|
||||||
|
permissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DebugNotifications.setCount(requireContext(), desired)
|
||||||
|
refreshDebugControls()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshDebugControls() {
|
||||||
|
val context = context ?: return
|
||||||
|
val count = DebugNotifications.count(context)
|
||||||
|
binding?.debugCount?.text = count.toString()
|
||||||
|
binding?.debugCycle?.isEnabled = count > 0
|
||||||
|
updatingCycle = true
|
||||||
|
binding?.debugCycle?.isChecked = DebugNotifications.isCycling(context)
|
||||||
|
updatingCycle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
override fun onDestroyView() { binding = null; super.onDestroyView() }
|
||||||
|
|||||||
+4
@@ -16,6 +16,7 @@ import se.ajpanton.notificationsmaster.alerts.AndroidAlertEffectPlayer
|
|||||||
import se.ajpanton.notificationsmaster.module.AlertPolicySync
|
import se.ajpanton.notificationsmaster.module.AlertPolicySync
|
||||||
import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore
|
import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore
|
||||||
import se.ajpanton.notificationsmaster.data.EncryptedImageStore
|
import se.ajpanton.notificationsmaster.data.EncryptedImageStore
|
||||||
|
import se.ajpanton.notificationsmaster.debug.DebugNotifications
|
||||||
import se.ajpanton.notificationsmaster.model.NotificationAction
|
import se.ajpanton.notificationsmaster.model.NotificationAction
|
||||||
import se.ajpanton.notificationsmaster.model.NotificationLogEntry
|
import se.ajpanton.notificationsmaster.model.NotificationLogEntry
|
||||||
import se.ajpanton.notificationsmaster.settings.LoggingRuleStore
|
import se.ajpanton.notificationsmaster.settings.LoggingRuleStore
|
||||||
@@ -59,6 +60,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
|||||||
super.onListenerConnected()
|
super.onListenerConnected()
|
||||||
AlertPolicySync.publishPlaybackState(this, true)
|
AlertPolicySync.publishPlaybackState(this, true)
|
||||||
getActiveNotifications()?.forEach { sbn ->
|
getActiveNotifications()?.forEach { sbn ->
|
||||||
|
if (DebugNotifications.isManaged(this, sbn)) return@forEach
|
||||||
val snapshot = NotificationContents.snapshot(sbn)
|
val snapshot = NotificationContents.snapshot(sbn)
|
||||||
SeenApps.markSeen(snapshot.packageName)
|
SeenApps.markSeen(snapshot.packageName)
|
||||||
activeNotifications[snapshot.key] = snapshot
|
activeNotifications[snapshot.key] = snapshot
|
||||||
@@ -73,6 +75,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
|
if (DebugNotifications.isManaged(this, sbn)) return
|
||||||
val snapshot = NotificationContents.snapshot(sbn)
|
val snapshot = NotificationContents.snapshot(sbn)
|
||||||
SeenApps.markSeen(snapshot.packageName)
|
SeenApps.markSeen(snapshot.packageName)
|
||||||
val previous = activeNotifications.put(snapshot.key, snapshot)
|
val previous = activeNotifications.put(snapshot.key, snapshot)
|
||||||
@@ -108,6 +111,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
|||||||
rankingMap: RankingMap,
|
rankingMap: RankingMap,
|
||||||
reason: Int,
|
reason: Int,
|
||||||
) {
|
) {
|
||||||
|
if (DebugNotifications.isManaged(this, sbn)) return
|
||||||
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
|
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
|
||||||
SeenApps.markSeen(snapshot.packageName)
|
SeenApps.markSeen(snapshot.packageName)
|
||||||
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
|
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.debug
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import se.ajpanton.notificationsmaster.MainActivity
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/** Posts real, silent notifications for testing icon filtering and layout. */
|
||||||
|
object DebugNotifications {
|
||||||
|
const val EXTRA_DEBUG = "se.ajpanton.notificationsmaster.DEBUG_NOTIFICATION"
|
||||||
|
const val MAX_COUNT = 50
|
||||||
|
|
||||||
|
private const val PREFS = "debug_notifications"
|
||||||
|
private const val KEY_COUNT = "count"
|
||||||
|
private const val KEY_CYCLE = "cycle"
|
||||||
|
private const val KEY_CURRENT = "current"
|
||||||
|
private const val KEY_DIRECTION = "direction"
|
||||||
|
private const val KEY_BOOT_EPOCH = "boot_epoch"
|
||||||
|
private const val KEY_RECOVERY_USED = "recovery_used"
|
||||||
|
private const val CHANNEL_ID = "debug_notification_icons"
|
||||||
|
private const val ID_BASE = 20_000
|
||||||
|
private const val GROUP_PREFIX = "notifications_master_debug_"
|
||||||
|
private const val CYCLE_DELAY = 2_000L
|
||||||
|
private const val RECOVERY_DELAY = 3_500L
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var cycleTask: Runnable? = null
|
||||||
|
private var recoveryTask: Runnable? = null
|
||||||
|
|
||||||
|
fun count(context: Context) = prefs(context).getInt(KEY_COUNT, 0).coerceIn(0, MAX_COUNT)
|
||||||
|
|
||||||
|
fun isCycling(context: Context) = prefs(context).getBoolean(KEY_CYCLE, false)
|
||||||
|
|
||||||
|
fun hasPermission(context: Context) =
|
||||||
|
context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
fun setCount(context: Context, count: Int) {
|
||||||
|
prefs(context).edit()
|
||||||
|
.putInt(KEY_COUNT, count.coerceIn(0, MAX_COUNT))
|
||||||
|
.putBoolean(KEY_RECOVERY_USED, false)
|
||||||
|
.apply()
|
||||||
|
apply(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCycling(context: Context, enabled: Boolean) {
|
||||||
|
prefs(context).edit()
|
||||||
|
.putBoolean(KEY_CYCLE, enabled)
|
||||||
|
.putInt(KEY_CURRENT, 0)
|
||||||
|
.putInt(KEY_DIRECTION, 1)
|
||||||
|
.putBoolean(KEY_RECOVERY_USED, false)
|
||||||
|
.apply()
|
||||||
|
apply(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun apply(context: Context) {
|
||||||
|
val app = context.applicationContext
|
||||||
|
val prefs = prefs(app)
|
||||||
|
resetRecoveryAfterBoot(prefs)
|
||||||
|
val configured = count(app)
|
||||||
|
val desired = if (isCycling(app)) prefs.getInt(KEY_CURRENT, 0).coerceIn(0, configured) else configured
|
||||||
|
val manager = app.getSystemService(NotificationManager::class.java)
|
||||||
|
cancelAbove(app, manager, desired)
|
||||||
|
if (hasPermission(app)) {
|
||||||
|
ensureChannel(manager)
|
||||||
|
repeat(desired) { manager.notify(ID_BASE + it + 1, notification(app, it + 1)) }
|
||||||
|
scheduleRecovery(app, desired)
|
||||||
|
}
|
||||||
|
scheduleCycle(app, configured)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDebug(notification: Notification) = notification.extras?.getBoolean(EXTRA_DEBUG, false) == true
|
||||||
|
|
||||||
|
fun isManaged(context: Context, sbn: StatusBarNotification) =
|
||||||
|
sbn.packageName == context.packageName && (
|
||||||
|
isDebug(sbn.notification) || sbn.id in (ID_BASE + 1)..(ID_BASE + MAX_COUNT) ||
|
||||||
|
(count(context) > 0 && sbn.notification.flags and Notification.FLAG_GROUP_SUMMARY != 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun notification(context: Context, number: Int): Notification {
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
val pending = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||||
|
)
|
||||||
|
return Notification.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(numberIcon(context, number))
|
||||||
|
.setContentTitle("Debug icon $number")
|
||||||
|
.setContentText("Notifications Master")
|
||||||
|
.setContentIntent(pending)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setCategory(Notification.CATEGORY_STATUS)
|
||||||
|
.setGroup("$GROUP_PREFIX$number")
|
||||||
|
.setSortKey(number.toString().padStart(2, '0'))
|
||||||
|
.setShortcutId("$GROUP_PREFIX$number")
|
||||||
|
.addExtras(android.os.Bundle().apply {
|
||||||
|
putBoolean(EXTRA_DEBUG, true)
|
||||||
|
putInt("number", number)
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.apply { flags = flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun numberIcon(context: Context, number: Int): Icon {
|
||||||
|
val size = (48 * context.resources.displayMetrics.density + 0.5f).toInt()
|
||||||
|
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
val background = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.HSVToColor(floatArrayOf(number * 37f % 360f, 0.6f, 0.9f))
|
||||||
|
}
|
||||||
|
canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), background)
|
||||||
|
val label = number.toString()
|
||||||
|
val text = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.WHITE
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
isFakeBoldText = true
|
||||||
|
textSize = size * if (label.length > 1) 0.45f else 0.6f
|
||||||
|
}
|
||||||
|
val bounds = Rect().also { text.getTextBounds(label, 0, label.length, it) }
|
||||||
|
canvas.drawText(label, size / 2f, size / 2f - bounds.exactCenterY(), text)
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureChannel(manager: NotificationManager) {
|
||||||
|
manager.createNotificationChannel(
|
||||||
|
NotificationChannel(CHANNEL_ID, "Debug notification icons", NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
description = "Silent notification icons used to test layouts"
|
||||||
|
setShowBadge(false)
|
||||||
|
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelAbove(context: Context, manager: NotificationManager, desired: Int) {
|
||||||
|
manager.activeNotifications.filter { it.packageName == context.packageName }.forEach { sbn ->
|
||||||
|
val number = sbn.id - ID_BASE
|
||||||
|
if (number in 1..MAX_COUNT && number > desired) cancel(manager, sbn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleCycle(context: Context, configured: Int) {
|
||||||
|
if (!isCycling(context) || configured == 0) {
|
||||||
|
cycleTask?.let(handler::removeCallbacks)
|
||||||
|
cycleTask = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (cycleTask != null) return
|
||||||
|
cycleTask = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
val prefs = prefs(context)
|
||||||
|
if (!prefs.getBoolean(KEY_CYCLE, false)) {
|
||||||
|
cycleTask = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val max = count(context)
|
||||||
|
var current = prefs.getInt(KEY_CURRENT, 0).coerceIn(0, max)
|
||||||
|
var direction = if (prefs.getInt(KEY_DIRECTION, 1) < 0) -1 else 1
|
||||||
|
current += direction
|
||||||
|
if (current >= max) { current = max; direction = -1 }
|
||||||
|
if (current <= 0) { current = 0; direction = 1 }
|
||||||
|
prefs.edit().putInt(KEY_CURRENT, current).putInt(KEY_DIRECTION, direction).apply()
|
||||||
|
apply(context)
|
||||||
|
handler.postDelayed(this, CYCLE_DELAY)
|
||||||
|
}
|
||||||
|
}.also { handler.postDelayed(it, CYCLE_DELAY) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scheduleRecovery(context: Context, desired: Int) {
|
||||||
|
recoveryTask?.let(handler::removeCallbacks)
|
||||||
|
recoveryTask = null
|
||||||
|
if (desired == 0 || prefs(context).getBoolean(KEY_RECOVERY_USED, false)) return
|
||||||
|
recoveryTask = Runnable {
|
||||||
|
val manager = context.getSystemService(NotificationManager::class.java)
|
||||||
|
val own = manager.activeNotifications.filter { it.packageName == context.packageName }
|
||||||
|
if (own.any { it.id !in (ID_BASE + 1)..(ID_BASE + MAX_COUNT) && !isDebug(it.notification) }) {
|
||||||
|
prefs(context).edit().putBoolean(KEY_RECOVERY_USED, true).apply()
|
||||||
|
own.forEach { cancel(manager, it) }
|
||||||
|
repeat(desired) { manager.notify(ID_BASE + it + 1, notification(context, it + 1)) }
|
||||||
|
}
|
||||||
|
}.also { handler.postDelayed(it, RECOVERY_DELAY) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetRecoveryAfterBoot(prefs: android.content.SharedPreferences) {
|
||||||
|
val bootEpoch = System.currentTimeMillis() - SystemClock.elapsedRealtime()
|
||||||
|
if (abs(prefs.getLong(KEY_BOOT_EPOCH, -1) - bootEpoch) > 5_000) {
|
||||||
|
prefs.edit().putLong(KEY_BOOT_EPOCH, bootEpoch).putBoolean(KEY_RECOVERY_USED, false).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancel(manager: NotificationManager, sbn: StatusBarNotification) {
|
||||||
|
sbn.tag?.let { manager.cancel(it, sbn.id) } ?: manager.cancel(sbn.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
@@ -27,5 +27,81 @@
|
|||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:text="Without optional system integration, Android keeps the source app’s own sound and vibration. Matching custom alerts play in addition; they do not replace it."
|
android:text="Without optional system integration, Android keeps the source app’s own sound and vibration. Matching custom alerts play in addition; they do not replace it."
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:text="@string/debug_notifications_title"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/debug_notifications_description"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/debug_cycle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:text="@string/debug_cycle_notifications" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/debug_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="0"
|
||||||
|
android:textSize="32sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/debug_minus_10"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="-10" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/debug_minus_1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="-1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/debug_plus_1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="+1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/debug_plus_10"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="+10" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/debug_reset"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/debug_reset_notifications" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
<string name="page_settings">Settings</string>
|
<string name="page_settings">Settings</string>
|
||||||
<string name="page_alerts">Alerts</string>
|
<string name="page_alerts">Alerts</string>
|
||||||
<string name="page_alert_status">Alert status</string>
|
<string name="page_alert_status">Alert status</string>
|
||||||
|
<string name="debug_notifications_title">Debug notifications</string>
|
||||||
|
<string name="debug_notifications_description">Post up to 50 real, silent notifications to test icon filtering and layout.</string>
|
||||||
|
<string name="debug_cycle_notifications">Cycle between zero and the selected count every two seconds</string>
|
||||||
|
<string name="debug_reset_notifications">Reset</string>
|
||||||
|
<string name="debug_notification_permission_required">Notification permission is required for debug notifications.</string>
|
||||||
<string name="page_profiles">Profiles</string>
|
<string name="page_profiles">Profiles</string>
|
||||||
<string name="page_rules">Rules</string>
|
<string name="page_rules">Rules</string>
|
||||||
<string name="page_apps">Apps</string>
|
<string name="page_apps">Apps</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user