diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bee873d0..cb991fa1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -110,6 +110,7 @@
android:exported="true">
+
diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
index 71b03092..15f76db7 100644
--- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt
+++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
@@ -342,6 +342,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
.apply()
}
+ fun getWebSocketReconnectRemindTime(): Long {
+ return sharedPrefs.getLong(SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME, WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS)
+ }
+
+ fun setWebSocketReconnectRemindTime(timeMillis: Long) {
+ sharedPrefs.edit()
+ .putLong(SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME, timeMillis)
+ .apply()
+ }
+
fun getDefaultBaseUrl(): String? {
return sharedPrefs.getString(SHARED_PREFS_DEFAULT_BASE_URL, null) ?:
sharedPrefs.getString(SHARED_PREFS_UNIFIED_PUSH_BASE_URL, null) // Fall back to UP URL, removed when default is set!
@@ -492,6 +502,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
+ const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime"
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL
const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL"
const val SHARED_PREFS_LAST_TOPICS = "LastTopics"
@@ -532,6 +543,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val WEBSOCKET_REMIND_TIME_ALWAYS = 1L
const val WEBSOCKET_REMIND_TIME_NEVER = Long.MAX_VALUE
+ const val WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS = 1L
+ const val WEBSOCKET_RECONNECT_REMIND_TIME_NEVER = Long.MAX_VALUE
+
private const val TAG = "NtfyRepository"
private var instance: Repository? = null
diff --git a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt
index 080e8482..20059d5c 100644
--- a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt
@@ -114,7 +114,27 @@ class WsConnection(
Log.d(TAG,"$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via alarm manager)")
val reconnectTime = Calendar.getInstance()
reconnectTime.add(Calendar.SECOND, seconds)
- alarmManager.setExact(AlarmManager.RTC_WAKEUP, reconnectTime.timeInMillis, RECONNECT_TAG, { start() }, null)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (alarmManager.canScheduleExactAlarms()) {
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP,
+ reconnectTime.timeInMillis,
+ RECONNECT_TAG,
+ { start() },
+ null
+ )
+ } else {
+ Log.d(TAG, "SCHEDULE_EXACT_ALARM permission denied: Failed to reschedule websocket connection")
+ }
+ } else {
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP,
+ reconnectTime.timeInMillis,
+ RECONNECT_TAG,
+ { start() },
+ null
+ )
+ }
} else {
Log.d(TAG, "$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via handler)")
val handler = Handler(Looper.getMainLooper())
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
index e131f32c..5ac2f7ad 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -3,6 +3,7 @@ package io.heckel.ntfy.ui
import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
+import android.app.AlarmManager
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Intent
@@ -11,6 +12,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
+import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
import android.text.method.LinkMovementMethod
import android.view.ActionMode
import android.view.Menu
@@ -125,9 +127,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
Log.addScrubTerm(s.topic)
}
- // Update banner + WebSocket banner
+ // Update battery banner + WebSocket banner + websocket reconnect banner
showHideBatteryBanner(subscriptions)
showHideWebSocketBanner(subscriptions)
+ showHideWebSocketReconnectBanner(subscriptions)
}
}
@@ -194,6 +197,38 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS)
SubscriberServiceManager(this).restart()
wsBanner.visibility = View.GONE
+
+ // maybe show WebSocketReconnectBanner
+ viewModel.list().observe(this) {
+ it?.let { subscriptions ->
+ showHideWebSocketReconnectBanner(subscriptions)
+ }
+ }
+ }
+
+ // WebSocket Reconnect banner
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val wsReconnectBanner = findViewById(R.id.main_banner_websocket_reconnect)
+ val wsReconnectText = findViewById(R.id.main_banner_websocket_reconnect_text)
+ val wsReconnectDismissButton =
+ findViewById