1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +05:30
Files
nikki/composeApp/src/androidMain/kotlin/com/avinal/memos/notifications/TaskNotificationManager.kt
T
avinal e4c19c2d7c Fix security issues from review
Notifications:
- VISIBILITY_PRIVATE on all channels and notifications
  (hides task text from lockscreen)
- Remove setFullScreenIntent (requires USE_FULL_SCREEN_INTENT
  permission; p1 channel already bypasses DND)

Auth:
- Clear cached token and server URL in memory on logout
  via AuthRepository.onLogout callback

Offline queue:
- Replace manual JSON string interpolation with
  kotlinx.serialization JsonObject/JsonPrimitive
  (prevents JSON injection from memo content)

CI/CD:
- Pin all GitHub Actions to commit SHAs
- Add permissions: contents: read to build workflow
- Decode keystore via env var instead of inline expansion
- Sanitize tag name through env var in release upload
- Fix test task name: testAndroidHostTest

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context)
2026-06-05 15:28:10 +05:30

119 lines
5.8 KiB
Kotlin

package com.avinal.memos.notifications
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.RingtoneManager
import androidx.core.app.NotificationCompat
object TaskNotificationManager {
private const val CHANNEL_P1 = "task_p1"
private const val CHANNEL_P2 = "task_p2"
private const val CHANNEL_P3 = "task_p3"
private const val CHANNEL_DEFAULT = "task_default"
fun createChannels(context: Context) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
val audioAttr = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
manager.createNotificationChannel(NotificationChannel(CHANNEL_P1, "P1 — Urgent", NotificationManager.IMPORTANCE_HIGH).apply {
description = "High priority — alarm sound, strong vibration, wakes screen"
enableVibration(true); vibrationPattern = longArrayOf(0, 500, 200, 500, 200, 500)
setSound(alarmUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true); setBypassDnd(true)
})
manager.createNotificationChannel(NotificationChannel(CHANNEL_P2, "P2 — Medium", NotificationManager.IMPORTANCE_HIGH).apply {
description = "Medium priority — notification sound, vibration"
enableVibration(true); vibrationPattern = longArrayOf(0, 300, 200, 300)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE; setShowBadge(true)
})
manager.createNotificationChannel(NotificationChannel(CHANNEL_P3, "P3 — Low", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "Low priority — notification sound, short vibration"
enableVibration(true); vibrationPattern = longArrayOf(0, 200)
setSound(soundUri, audioAttr); lockscreenVisibility = android.app.Notification.VISIBILITY_PRIVATE
})
manager.createNotificationChannel(NotificationChannel(CHANNEL_DEFAULT, "No Priority", NotificationManager.IMPORTANCE_LOW).apply {
description = "No priority — silent notification"
enableVibration(false); setSound(null, null)
})
manager.deleteNotificationChannel("task_reminders")
}
fun showTaskNotification(
context: Context,
notificationId: Int,
taskText: String,
dueLabel: String,
priority: Int = 0,
) {
val openIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
} ?: Intent()
val pendingOpen = PendingIntent.getActivity(
context, notificationId, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val channelId = when (priority) { 1 -> CHANNEL_P1; 2 -> CHANNEL_P2; 3 -> CHANNEL_P3; else -> CHANNEL_DEFAULT }
val notifPriority = when (priority) { 1 -> NotificationCompat.PRIORITY_MAX; 2 -> NotificationCompat.PRIORITY_HIGH; 3 -> NotificationCompat.PRIORITY_DEFAULT; else -> NotificationCompat.PRIORITY_LOW }
val priorityEmoji = when (priority) { 1 -> "🔴"; 2 -> "🟠"; 3 -> "🔵"; else -> "" }
val priorityTag = when (priority) { 1 -> "URGENT"; 2 -> "MEDIUM"; 3 -> "LOW"; else -> "" }
val title = buildString {
if (priorityEmoji.isNotEmpty()) append("$priorityEmoji ")
append(taskText)
}
val bigText = buildString {
append(dueLabel)
if (priorityTag.isNotEmpty()) append("\nPriority: $priorityTag")
}
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_popup_reminder)
.setContentTitle(title)
.setContentText(dueLabel)
.setStyle(NotificationCompat.BigTextStyle().bigText(bigText))
.setSubText(when (priority) { 1 -> "p1 urgent"; 2 -> "p2 medium"; 3 -> "p3 low"; else -> "nikki" })
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
.setPriority(notifPriority)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setAutoCancel(true)
.setOnlyAlertOnce(false)
.setContentIntent(pendingOpen)
.setDefaults(NotificationCompat.DEFAULT_LIGHTS)
.setGroup("task_reminders_${notificationId}")
.setColor(when (priority) { 1 -> 0xFFE51400.toInt(); 2 -> 0xFFF0A30A.toInt(); 3 -> 0xFF1BA1E2.toInt(); else -> 0xFF666666.toInt() })
when (priority) {
1 -> {
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
builder.setVibrate(longArrayOf(0, 500, 200, 500, 200, 500))
}
2 -> {
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
builder.setVibrate(longArrayOf(0, 300, 200, 300))
}
3 -> {
builder.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
builder.setVibrate(longArrayOf(0, 200))
}
}
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(notificationId, builder.build())
}
}