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:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user