This commit is contained in:
2025-10-19 21:51:00 +08:00
parent 7b60fb965f
commit c7d738ccce
11 changed files with 624 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
package com.taskttl.core.alarm
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.taskttl.core.notification.NotificationManager
import com.taskttl.core.notification.NotificationPayload
import com.taskttl.core.notification.NotificationRepeatType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != "com.taskttl.ALARM_TRIGGER") return
val id = intent.getStringExtra("id") ?: return
val title = intent.getStringExtra("title") ?: "TaskTTL"
val message = intent.getStringExtra("message") ?: ""
// 使用协程调用统一通知方法
CoroutineScope(Dispatchers.Main).launch {
NotificationManager.scheduleNotification(
NotificationPayload(
id = id,
title = title,
message = message,
triggerTimeMillis = System.currentTimeMillis(),
repeatType = NotificationRepeatType.NONE
)
)
}
// val id = intent.getStringExtra("id") ?: return
// val title = intent.getStringExtra("title") ?: "TaskTTL"
// val message = intent.getStringExtra("message") ?: ""
//
// val channelId = "taskttl_channel"
//
// // 确保通道存在
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
// if (nm.getNotificationChannel(channelId) == null) {
// val channel = android.app.NotificationChannel(
// channelId,
// "TaskTTL Notifications",
// android.app.NotificationManager.IMPORTANCE_HIGH
// )
// channel.description = "TaskTTL 提醒通知"
// nm.createNotificationChannel(channel)
// }
// }
//
// // 构建通知
// val builder = NotificationCompat.Builder(context, channelId)
// .setContentTitle(title)
// .setContentText(message)
// .setSmallIcon(R.mipmap.ic_launcher)
// .setAutoCancel(true)
// .setPriority(NotificationCompat.PRIORITY_HIGH)
//
// val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
// nm.notify(id.hashCode(), builder.build())
}
}

View File

@@ -0,0 +1,34 @@
package com.taskttl.core.alarm
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.taskttl.core.notification.NotificationPayload
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// 重新注册所有闹钟(可从数据库恢复)
// 这里假设你有一个持久化存储保存了待触发通知列表
// 例如 Room / SharedPreferences / 数据库
// val pendingNotifications = loadPendingNotifications(context)
//
// // 使用协程重新注册通知
// CoroutineScope(Dispatchers.Main).launch {
// pendingNotifications.forEach { payload ->
// NotificationManager.scheduleNotification(payload)
// }
// }
}
}
/**
* 从存储中获取设备重启后需要重新注册的通知
*/
private fun loadPendingNotifications(context: Context): List<NotificationPayload> {
// TODO: 这里你需要实现真实数据恢复逻辑
// 示例:返回一个空列表
return emptyList()
}
}

View File

@@ -0,0 +1,197 @@
package com.taskttl.core.notification
import android.Manifest
import android.app.Activity
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.media.RingtoneManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import com.taskttl.MainApplication
import com.taskttl.R
import com.taskttl.core.alarm.AlarmReceiver
import com.taskttl.core.utils.LogUtils
import java.util.concurrent.TimeUnit
import kotlin.math.max
actual object NotificationManager {
private val channelId = "taskttl_channel"
init {
setupNotificationChannel()
}
private fun getActivity(): Activity = MainApplication.currentActivity()
?: throw IllegalStateException("No activity available")
/** 创建通知通道Android 8+ */
private fun setupNotificationChannel() {
val activity = MainApplication.currentActivity() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
if (nm.getNotificationChannel(channelId) == null) {
val channel = android.app.NotificationChannel(
channelId,
"TaskTTL Notifications",
android.app.NotificationManager.IMPORTANCE_HIGH
).apply { description = "TaskTTL 提醒通知" }
nm.createNotificationChannel(channel)
LogUtils.d("DevTTL_NotificationTest", "Notification channel created")
}
}
}
/** 调度通知 */
actual suspend fun scheduleNotification(payload: NotificationPayload) {
val activity = getActivity()
// Android 13+ 权限检查
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1001
)
return
}
val delay = max(0, payload.triggerTimeMillis - System.currentTimeMillis())
if (delay == 0L) {
showImmediateNotification(payload)
return
}
if (delay <= 24 * 60 * 60 * 1000L && payload.repeatType == NotificationRepeatType.NONE) {
scheduleExactAlarm(payload)
} else {
scheduleWorkManager(payload)
}
}
/** AlarmManager 精确闹钟 */
private fun scheduleExactAlarm(payload: NotificationPayload) {
val activity = getActivity()
val alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
scheduleWorkManager(payload) // 回退 WorkManager
return
}
val intent = Intent(activity, AlarmReceiver::class.java).apply {
action = "com.taskttl.ALARM_TRIGGER"
putExtra("id", payload.id)
putExtra("title", payload.title)
putExtra("message", payload.message)
}
val pendingIntent = PendingIntent.getBroadcast(
activity,
payload.id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
payload.triggerTimeMillis,
pendingIntent
)
}
/** WorkManager 长期/周期通知 */
private fun scheduleWorkManager(payload: NotificationPayload) {
val activity = getActivity()
val data = workDataOf(
"id" to payload.id,
"title" to payload.title,
"message" to payload.message,
"repeatType" to payload.repeatType.name
)
val workManager = WorkManager.getInstance(activity)
if (payload.repeatType == NotificationRepeatType.NONE) {
val delay = max(0, payload.triggerTimeMillis - System.currentTimeMillis())
val request = OneTimeWorkRequestBuilder<NotificationWorker>()
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(data)
.addTag(payload.id)
.build()
workManager.enqueue(request)
} else {
val interval = when (payload.repeatType) {
NotificationRepeatType.DAILY -> 24 * 60 * 60 * 1000L
NotificationRepeatType.WEEKLY -> 7 * 24 * 60 * 60 * 1000L
NotificationRepeatType.MONTHLY -> 30 * 24 * 60 * 60 * 1000L
else -> 24 * 60 * 60 * 1000L
}
val request = PeriodicWorkRequestBuilder<NotificationWorker>(interval, TimeUnit.MILLISECONDS)
.setInputData(data)
.addTag(payload.id)
.build()
workManager.enqueueUniquePeriodicWork(
payload.id,
ExistingPeriodicWorkPolicy.UPDATE,
request
)
}
}
/** 立即显示通知 */
private fun showImmediateNotification(payload: NotificationPayload) {
val activity = getActivity()
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
val notification = NotificationCompat.Builder(activity, channelId)
.setContentTitle(payload.title)
.setContentText(payload.message)
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
// .setDefaults(NotificationCompat.DEFAULT_SOUND)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.build()
nm.notify(payload.id.hashCode(), notification)
}
/** 取消通知 */
actual suspend fun cancelNotification(id: String) {
val activity = getActivity()
WorkManager.getInstance(activity).cancelAllWorkByTag(id)
val alarmManager = activity.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(activity, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
activity,
id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
nm.cancel(id.hashCode())
}
actual suspend fun cancelAll() {
val activity = getActivity()
WorkManager.getInstance(activity).cancelAllWork()
val nm = activity.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
nm.cancelAll()
}
}

View File

@@ -0,0 +1,123 @@
package com.taskttl.core.notification
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.taskttl.MainApplication
import androidx.core.content.edit
actual object NotificationPermissionManager {
private const val REQUEST_CODE = 2025
private var callback: NotificationPermissionCallback? = null
/**
* 在 Activity 的 onRequestPermissionsResult 中调用
*/
fun handlePermissionResult(requestCode: Int, grantResults: IntArray) {
if (requestCode == REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
callback?.onGranted()
} else {
callback?.onDenied()
}
}
}
// expect/actual 适配Android 需要 Activity 参数
actual fun requestPermission(callback: NotificationPermissionCallback) {
// Android 13 以下无需权限
if (Build.VERSION.SDK_INT < 33) {
callback.onGranted()
return
}
val activity = MainApplication.currentActivity()
if (activity == null) {
Log.w("NotificationPermission", "No current activity found")
callback.onDenied()
return
}
this.callback = callback
val prefs = activity.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
val permission = Manifest.permission.POST_NOTIFICATIONS
val granted = ContextCompat.checkSelfPermission(
activity, permission
) == PackageManager.PERMISSION_GRANTED
if (granted) {
callback.onGranted()
} else {
val isFirstRequest = prefs.getBoolean("notification_first_request", true)
val shouldShowRationale =
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
if (!shouldShowRationale && !isFirstRequest) {
openNotificationSettings()
callback.onDenied()
} else {
ActivityCompat.requestPermissions(activity, arrayOf(permission), REQUEST_CODE)
}
}
prefs.edit { putBoolean("notification_first_request", false) }
}
actual fun verifyPermission(): Boolean {
// Android 13 以下无需权限
if (Build.VERSION.SDK_INT < 33) {
return true
}
val activity = MainApplication.currentActivity()
if (activity == null) {
Log.w("NotificationPermission", "No current activity found")
return false
}
val permission = Manifest.permission.POST_NOTIFICATIONS
val granted = ContextCompat.checkSelfPermission(
activity, permission
) == PackageManager.PERMISSION_GRANTED
return granted
}
/**
* 引导用户到系统设置页关闭通知权限
*/
actual fun disablePermission() {
openNotificationSettings()
}
private fun openNotificationSettings() {
val context = MainApplication.currentActivity() ?: MainApplication.instance
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API 26 及以上,打开通知设置页
Intent().apply {
action = android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS
putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, context.packageName)
}
} else {
// API 26 以下,打开应用详情页
Intent().apply {
action = android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = "package:${context.packageName}".toUri()
}
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(intent)
Log.d("NotificationPermission", "打开通知设置页")
} catch (e: Exception) {
Log.e("NotificationPermission", "无法打开通知设置页: ${e.message}")
}
}
}

View File

@@ -0,0 +1,54 @@
package com.taskttl.core.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.RingtoneManager
import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.taskttl.MainActivity
import com.taskttl.R
class NotificationWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val id = inputData.getString("id") ?: return Result.failure()
val title = inputData.getString("title") ?: "TaskTTL"
val message = inputData.getString("message") ?: ""
sendNotification(id, title, message)
return Result.success()
}
private fun sendNotification(id: String, title: String, message: String) {
val context = applicationContext
val channelId = "taskttl_channel"
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
id.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, channelId)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
.setPriority(NotificationCompat.PRIORITY_HIGH)
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
nm.notify(id.hashCode(), builder.build())
}
}

View File

@@ -0,0 +1,27 @@
package com.taskttl.core.notification
/**
* 通知管理器
* @author admin
* @date 2025/10/16
* @constructor 创建[NotificationManager]
*/
expect object NotificationManager {
/**
* 日程通知
* @param [payload] 有效载荷
*/
suspend fun scheduleNotification(payload: NotificationPayload)
/**
* 取消通知
* @param [id] ID
*/
suspend fun cancelNotification(id: String)
/**
* 全部取消
*/
suspend fun cancelAll()
}

View File

@@ -0,0 +1,20 @@
package com.taskttl.core.notification
/**
* 通知有效载荷
* @author admin
* @date 2025/10/16
* @constructor 创建[NotificationPayload]
* @param [id] ID
* @param [title] 标题
* @param [message] 消息
* @param [triggerTimeMillis] 触发时间毫秒
* @param [repeatType] 重复类型
*/
data class NotificationPayload(
val id: String,
val title: String,
val message: String,
val triggerTimeMillis: Long,
val repeatType: NotificationRepeatType = NotificationRepeatType.NONE,
)

View File

@@ -0,0 +1,31 @@
package com.taskttl.core.notification
/**
* 通知权限回调接口
*/
interface NotificationPermissionCallback {
/**
* 授予
*/
fun onGranted()
/**
* 被否认
*/
fun onDenied()
}
/**
* 跨平台通知权限处理器
*/
expect object NotificationPermissionManager {
/**
* 请求通知权限
* @return 是否允许
*/
fun requestPermission(callback: NotificationPermissionCallback)
fun verifyPermission(): Boolean
fun disablePermission()
}

View File

@@ -0,0 +1,14 @@
package com.taskttl.core.notification
/**
* 通知重复类型
* @author admin
* @date 2025/10/16
* @constructor 创建[NotificationRepeatType]
*/
enum class NotificationRepeatType {
NONE,
DAILY,
WEEKLY,
MONTHLY
}

View File

@@ -0,0 +1,43 @@
package com.taskttl.core.notification
import platform.UserNotifications.*
actual class NotificationManager {
actual suspend fun scheduleNotification(payload: NotificationPayload) {
val content = UNMutableNotificationContent().apply {
title = payload.title
body = payload.message
sound = UNNotificationSound.defaultSound()
}
val interval = (payload.triggerTimeMillis - getCurrentTimeMillis()) / 1000.0
val trigger = UNTimeIntervalNotificationTrigger.triggerWithTimeInterval(
interval,
repeats = payload.repeatType != NotificationRepeatType.NONE
)
val request = UNNotificationRequest.requestWithIdentifier(
payload.id,
content,
trigger
)
UNUserNotificationCenter.currentNotificationCenter()
.addNotificationRequest(request) { error ->
error?.let { println("❌ Notification Error: ${it.localizedDescription}") }
}
}
actual suspend fun cancelNotification(id: String) {
UNUserNotificationCenter.currentNotificationCenter()
.removePendingNotificationRequestsWithIdentifiers(listOf(id))
}
actual suspend fun cancelAll() {
UNUserNotificationCenter.currentNotificationCenter()
.removeAllPendingNotificationRequests()
}
private fun getCurrentTimeMillis(): Long =
(platform.Foundation.NSDate().timeIntervalSince1970 * 1000).toLong()
}

View File

@@ -0,0 +1,14 @@
package com.taskttl.core.notification
import platform.UserNotifications.*
actual object NotificationPermissionManager {
actual fun requestPermission(callback: NotificationPermissionCallback) {
val center = UNUserNotificationCenter.currentNotificationCenter()
center.requestAuthorizationWithOptions(
UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge
) { granted, _ ->
if (granted) callback.onGranted() else callback.onDenied()
}
}
}