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