fix(android): add AlarmManager 2-min background notification polling

- AlarmScheduler.kt: schedules setExactAndAllowWhileIdle every 2 min
- AlarmReceiver.kt: reads FlutterSharedPreferences, polls LAN backend, posts native notifications, self-chains next alarm
- MainActivity.kt: schedule alarm on app open
- BootReceiver.kt: schedule alarm on device reboot
- AndroidManifest.xml: register AlarmReceiver
This commit is contained in:
ssbg04 committed 2026-08-18 13:19:11 +08:00
1 parent 4306a19d7b
commit 648280ab45
5 files changed
+182 -1

No files matched your search

@@ -42,6 +42,9 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<receiver
android:name=".AlarmReceiver"
android:exported="false"/>
<activity
android:name=".SplashActivity"
android:exported="true"
@@ -0,0 +1,131 @@
package com.teamsb.tis_rms
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import org.json.JSONArray
import java.net.HttpURLConnection
import java.net.URL
class AlarmReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "AlarmReceiver"
private const val CHANNEL_ID = "tis_rms_activities_channel"
private const val PREFS_NAME = "FlutterSharedPreferences"
private const val KEY_SERVER_URL = "flutter.server_url"
private const val KEY_TOKEN = "flutter.jwt_token"
private const val KEY_LAST_ID = "flutter.last_seen_notification_id"
private const val TIMEOUT_MS = 4000
}
override fun onReceive(context: Context, intent: Intent) {
val pendingResult = goAsync()
Thread {
try {
checkAndNotify(context)
} catch (e: Exception) {
Log.e(TAG, "check failed: ${e.message}")
} finally {
AlarmScheduler.schedule(context)
pendingResult.finish()
}
}.start()
}
private fun checkAndNotify(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val rawUrl = prefs.getString(KEY_SERVER_URL, "") ?: ""
if (rawUrl.isEmpty()) return
val token = prefs.getString(KEY_TOKEN, "") ?: ""
if (token.isEmpty()) return
val clean = rawUrl.trimEnd('/')
val baseUrl = if (clean.endsWith("/api")) clean else "$clean/api"
val conn = (URL("$baseUrl/notifications").openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = TIMEOUT_MS
readTimeout = TIMEOUT_MS
setRequestProperty("Authorization", "Bearer $token")
}
if (conn.responseCode != 200) {
conn.disconnect()
return
}
val body = conn.inputStream.bufferedReader().readText()
conn.disconnect()
val list = JSONArray(body)
if (list.length() == 0) return
val highestOldId = prefs.getInt(KEY_LAST_ID, 0)
var newHighestId = highestOldId
ensureChannel(context)
val notifManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
for (i in 0 until list.length()) {
val item = list.getJSONObject(i)
val id = item.optInt("id", 0)
// Handle is_read as boolean or integer (0/1)
val isReadRaw = item.opt("is_read")
val isRead = isReadRaw == true || isReadRaw == 1
val isNew = if (highestOldId == 0) !isRead else (id > highestOldId && !isRead)
if (!isNew) continue
val title = item.optString("title", "TIS RMS Notification")
val message = item.optString("message", "")
val launchIntent = context.packageManager
.getLaunchIntentForPackage(context.packageName)
?.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP }
val pi = PendingIntent.getActivity(
context, id, launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title)
.setContentText(message)
.setAutoCancel(true)
.setContentIntent(pi)
.build()
notifManager.notify(id, notification)
Log.d(TAG, "notified: [$id] $title")
if (id > newHighestId) newHighestId = id
}
if (newHighestId > highestOldId) {
prefs.edit().putInt(KEY_LAST_ID, newHighestId).apply()
}
}
private fun ensureChannel(context: Context) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
val channel = NotificationChannel(
CHANNEL_ID,
"Recent Activities",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications for recent activities and system events"
enableVibration(true)
}
manager.createNotificationChannel(channel)
}
}
@@ -0,0 +1,40 @@
package com.teamsb.tis_rms
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import android.util.Log
object AlarmScheduler {
private const val TAG = "AlarmScheduler"
const val ACTION = "com.teamsb.tis_rms.ALARM_NOTIFICATION_CHECK"
private const val INTERVAL_MS = 2 * 60 * 1000L // 2 minutes
fun schedule(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pi = buildPendingIntent(context)
alarmManager.cancel(pi)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + INTERVAL_MS,
pi
)
Log.d(TAG, "scheduled: next check in ${INTERVAL_MS / 1000}s")
}
fun cancel(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.cancel(buildPendingIntent(context))
Log.d(TAG, "cancelled")
}
private fun buildPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, AlarmReceiver::class.java).apply { action = ACTION }
return PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
}
@@ -13,6 +13,7 @@ class BootReceiver : BroadcastReceiver() {
) {
val serviceIntent = Intent(context, ForegroundService::class.java)
context.startForegroundService(serviceIntent)
AlarmScheduler.schedule(context)
}
}
}
@@ -1,5 +1,11 @@
package com.teamsb.tis_rms
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AlarmScheduler.schedule(this)
}
}