Merge branch 'main' of github.com:binwiederhier/ntfy-android into mtls
This commit is contained in:
commit
5c8cef27ae
8 changed files with 137 additions and 28 deletions
|
|
@ -6,7 +6,14 @@ interface Connection {
|
|||
fun since(): String?
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a unique connection identifier that changes every time a
|
||||
* connection needs to be re-established.
|
||||
*/
|
||||
data class ConnectionId(
|
||||
val baseUrl: String,
|
||||
val topicsToSubscriptionIds: Map<String, Long>
|
||||
val topicsToSubscriptionIds: Map<String, Long>,
|
||||
val connectionProtocol: String,
|
||||
val credentialsHash: Int, // Hash of "username:password" or 0 if no user
|
||||
val headersHash: Int // Hash of sorted headers or 0 if none
|
||||
)
|
||||
|
|
|
|||
|
|
@ -206,9 +206,27 @@ class SubscriberService : Service() {
|
|||
val instantSubscriptions = repository.getSubscriptions()
|
||||
.filter { s -> s.instant }
|
||||
val activeConnectionIds = connections.keys().toList().toSet()
|
||||
val connectionProtocol = repository.getConnectionProtocol()
|
||||
val desiredConnectionIds = instantSubscriptions // Set<ConnectionId>
|
||||
.groupBy { s -> ConnectionId(s.baseUrl, emptyMap()) }
|
||||
.map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) }
|
||||
.groupBy { s -> s.baseUrl }
|
||||
.map { (baseUrl, subs) ->
|
||||
// Create a unique connection ID for each base URL. Each change in the connection ID will
|
||||
// trigger a new connection, and close existing connections. We want to make sure that when the
|
||||
// connection protocol (JSON/WS), the user or the custom headers are updated, that we kill existing
|
||||
// connections and start new ones.
|
||||
val credentialsHash = repository.getUser(baseUrl)?.let { "${it.username}:${it.password}".hashCode() } ?: 0
|
||||
val headersHash = repository.getCustomHeadersForServer(baseUrl)
|
||||
.sortedBy { "${it.name}:${it.value}" }
|
||||
.joinToString(",") { "${it.name}:${it.value}" }
|
||||
.hashCode()
|
||||
ConnectionId(
|
||||
baseUrl = baseUrl,
|
||||
topicsToSubscriptionIds = subs.associate { s -> s.topic to s.id },
|
||||
connectionProtocol = connectionProtocol,
|
||||
credentialsHash = credentialsHash,
|
||||
headersHash = headersHash
|
||||
)
|
||||
}
|
||||
.toSet()
|
||||
val newConnectionIds = desiredConnectionIds.subtract(activeConnectionIds)
|
||||
val obsoleteConnectionIds = activeConnectionIds.subtract(desiredConnectionIds)
|
||||
|
|
@ -237,7 +255,7 @@ class SubscriberService : Service() {
|
|||
val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none"
|
||||
val serviceActive = { isServiceStarted }
|
||||
val user = repository.getUser(connectionId.baseUrl)
|
||||
val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) {
|
||||
val connection = if (connectionId.connectionProtocol == Repository.CONNECTION_PROTOCOL_WS) {
|
||||
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
|
||||
WsConnection(this, connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -26,12 +26,6 @@ class SubscriberServiceManager(private val context: Context) {
|
|||
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
|
||||
}
|
||||
|
||||
fun restart() {
|
||||
Intent(context, SubscriberService::class.java).also { intent ->
|
||||
context.stopService(intent) // Service will auto-restart
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or stops the foreground service by figuring out how many instant delivery subscriptions
|
||||
* exist. If there's > 0, then we need a foreground service.
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
// Update battery banner + WebSocket banner + websocket reconnect banner
|
||||
showHideBatteryBanner(subscriptions)
|
||||
showHideWebSocketBanner(subscriptions)
|
||||
showHideWebSocketReconnectBanner(subscriptions)
|
||||
showHideWebSocketReconnectBanner()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -304,13 +304,13 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
wsEnableButton.setOnClickListener {
|
||||
repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS)
|
||||
SubscriberServiceManager(this).restart()
|
||||
SubscriberServiceManager(this).refresh()
|
||||
wsBanner.visibility = View.GONE
|
||||
|
||||
// Maybe show WebSocketReconnectBanner
|
||||
viewModel.list().observe(this) {
|
||||
it?.let { subscriptions ->
|
||||
showHideWebSocketReconnectBanner(subscriptions)
|
||||
showHideWebSocketReconnectBanner()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -404,15 +404,14 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
}
|
||||
|
||||
private fun showHideWebSocketReconnectBanner(subscriptions: List<Subscription>) {
|
||||
private fun showHideWebSocketReconnectBanner() {
|
||||
val wsReconnectBanner = findViewById<View>(R.id.main_banner_websocket_reconnect)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val hasSelfHostedSubscriptions = subscriptions.count { it.baseUrl != appBaseUrl } > 0
|
||||
val usingWebSockets = repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS
|
||||
val wsReconnectRemindTimeReached = repository.getWebSocketReconnectRemindTime() < System.currentTimeMillis()
|
||||
val canScheduleExactAlarms = (getSystemService(ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()
|
||||
val showBanner = hasSelfHostedSubscriptions && wsReconnectRemindTimeReached && usingWebSockets && !canScheduleExactAlarms
|
||||
Log.d(TAG, "hasSelfHostedSubscriptions: ${hasSelfHostedSubscriptions}, wsReconnectRemindTimeReached: ${wsReconnectRemindTimeReached}, usingWebSockets: ${usingWebSockets}, canScheduleExactAlarms: ${canScheduleExactAlarms}")
|
||||
val showBanner = wsReconnectRemindTimeReached && usingWebSockets && !canScheduleExactAlarms
|
||||
Log.d(TAG, "wsReconnectRemindTimeReached: ${wsReconnectRemindTimeReached}, usingWebSockets: ${usingWebSockets}, canScheduleExactAlarms: ${canScheduleExactAlarms}")
|
||||
wsReconnectBanner.visibility = if (showBanner) View.VISIBLE else View.GONE
|
||||
} else {
|
||||
wsReconnectBanner.visibility = View.GONE
|
||||
|
|
|
|||
|
|
@ -700,7 +700,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
override fun putString(key: String?, value: String?) {
|
||||
val proto = value ?: repository.getConnectionProtocol()
|
||||
repository.setConnectionProtocol(proto)
|
||||
restartService()
|
||||
serviceManager.refresh() // Refresh to switch connections between WS and JSON stream
|
||||
}
|
||||
override fun getString(key: String?, defValue: String?): String {
|
||||
return repository.getConnectionProtocol()
|
||||
|
|
@ -737,10 +737,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
repository.setAutoDownloadMaxSize(autoDownloadSelectionCopy)
|
||||
}
|
||||
|
||||
private fun restartService() {
|
||||
serviceManager.restart() // Service will auto-restart
|
||||
}
|
||||
|
||||
private fun copyLogsToClipboard(scrub: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val context = context ?: return@launch
|
||||
|
|
@ -845,7 +841,8 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
}
|
||||
|
||||
fun updateExactAlarmsPref() {
|
||||
val exactAlarmsPrefId = context?.getString(R.string.settings_advanced_exact_alarms_key) ?: return
|
||||
val context = context ?: return
|
||||
val exactAlarmsPrefId = context.getString(R.string.settings_advanced_exact_alarms_key)
|
||||
val exactAlarmsPref: Preference? = findPreference(exactAlarmsPrefId)
|
||||
val canScheduleExactAlarms = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
(activity?.getSystemService(ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()
|
||||
|
|
@ -859,6 +856,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
}
|
||||
true
|
||||
}
|
||||
// Android doesn't show "ntfy" in the "Alarms & reminders" list if battery optimizations are disabled.
|
||||
//
|
||||
// In fact, if the user has granted the battery optimization exemption (see battery banner in MainActivity),
|
||||
// the alarm manager's canScheduleExactAlarms() method will return true.
|
||||
//
|
||||
// This is undocumented behavior. See https://github.com/binwiederhier/ntfy/issues/1456#issuecomment-3707174262
|
||||
exactAlarmsPref?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
&& !isIgnoringBatteryOptimizations(context)
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
|
@ -1047,7 +1052,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
override fun onUpdateUser(dialog: DialogFragment, user: User) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
repository.updateUser(user)
|
||||
serviceManager.restart() // Editing does not change the user ID
|
||||
serviceManager.refresh()
|
||||
runOnUiThread {
|
||||
if (this@SettingsActivity::userSettingsFragment.isInitialized) {
|
||||
userSettingsFragment.reload()
|
||||
|
|
@ -1059,7 +1064,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
override fun onDeleteUser(dialog: DialogFragment, baseUrl: String) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
repository.deleteUser(baseUrl)
|
||||
serviceManager.restart()
|
||||
serviceManager.refresh()
|
||||
runOnUiThread {
|
||||
if (this@SettingsActivity::userSettingsFragment.isInitialized) {
|
||||
userSettingsFragment.reload()
|
||||
|
|
@ -1071,7 +1076,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
override fun onAddCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
repository.addCustomHeader(header)
|
||||
serviceManager.restart() // Restart to apply new headers
|
||||
serviceManager.refresh() // Refresh to apply new headers
|
||||
runOnUiThread {
|
||||
if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) {
|
||||
customHeaderSettingsFragment.reload()
|
||||
|
|
@ -1083,7 +1088,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
override fun onUpdateCustomHeader(dialog: DialogFragment, oldHeader: io.heckel.ntfy.db.CustomHeader, newHeader: io.heckel.ntfy.db.CustomHeader) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
repository.updateCustomHeader(oldHeader, newHeader)
|
||||
serviceManager.restart() // Restart to apply header changes
|
||||
serviceManager.refresh() // Refresh to apply header changes
|
||||
runOnUiThread {
|
||||
if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) {
|
||||
customHeaderSettingsFragment.reload()
|
||||
|
|
@ -1095,7 +1100,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
override fun onDeleteCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
repository.deleteCustomHeader(header)
|
||||
serviceManager.restart()
|
||||
serviceManager.refresh()
|
||||
runOnUiThread {
|
||||
if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) {
|
||||
customHeaderSettingsFragment.reload()
|
||||
|
|
|
|||
|
|
@ -346,4 +346,63 @@
|
|||
<string name="settings_advanced_exact_alarms_title">התראות מדויקות</string>
|
||||
<string name="settings_advanced_exact_alarms_true">ntfy יכול לתזמן התראות מדויקות, התראות מדויקות משמשות כדי לחבר את ה־WebSockets מחדש ברקע. לחיצה כאן תשלול את ההרשאה.</string>
|
||||
<string name="settings_advanced_exact_alarms_false">ntfy לא יכול לתזמן התראות מדויקות. התראות מדויקות נחוצות כדי לחבר את ה־WebSockets מחדש ברקע. לחיצה תעניק את ההרשאה.</string>
|
||||
<string name="publish_dialog_title">פרסום אל %1$s</string>
|
||||
<string name="publish_dialog_title_hint">כותרת</string>
|
||||
<string name="publish_dialog_title_placeholder">למשל: מישהו בדלת</string>
|
||||
<string name="publish_dialog_message_hint">הודעה</string>
|
||||
<string name="publish_dialog_tags_hint">תגיות</string>
|
||||
<string name="publish_dialog_tags_placeholder">למשל: אזהרה, גולגולת</string>
|
||||
<string name="publish_dialog_priority_hint">עדיפות</string>
|
||||
<string name="publish_dialog_button_publish">פרסום</string>
|
||||
<string name="publish_dialog_error_sending">לא ניתן לפרסם הודעה: %1$s</string>
|
||||
<string name="publish_dialog_error_server">לא ניתן לפרסם הודעה: %1$s (קוד %2$d)</string>
|
||||
<string name="publish_dialog_message_published">ההודעה פורסמה</string>
|
||||
<string name="publish_dialog_uploading">העלאה: %1$s (%2$s / %3$s)</string>
|
||||
<string name="publish_dialog_upload_cancelled">ההעלאה בוטלה</string>
|
||||
<string name="publish_dialog_chip_title">כותרת</string>
|
||||
<string name="publish_dialog_chip_tags">תגיות</string>
|
||||
<string name="publish_dialog_chip_priority">עדיפות</string>
|
||||
<string name="publish_dialog_chip_click_url">לחיצה על כתובת</string>
|
||||
<string name="publish_dialog_chip_email">דוא״ל</string>
|
||||
<string name="publish_dialog_chip_delay">השהיה</string>
|
||||
<string name="publish_dialog_chip_markdown">Markdown</string>
|
||||
<string name="publish_dialog_chip_attach_url">צירוף לפי כתובת</string>
|
||||
<string name="publish_dialog_chip_attach_file">צירוף קובץ מקומי</string>
|
||||
<string name="publish_dialog_chip_phone_call">שיחת טלפון</string>
|
||||
<string name="publish_dialog_click_url_hint">לחיצה על כתובת</string>
|
||||
<string name="publish_dialog_click_url_placeholder">למשל: https://example.com/alerts/1234</string>
|
||||
<string name="publish_dialog_email_hint">דוא״ל</string>
|
||||
<string name="publish_dialog_email_placeholder">למשל: phil@example.com</string>
|
||||
<string name="publish_dialog_delay_hint">השהיית מסירה</string>
|
||||
<string name="publish_dialog_attach_url_placeholder">למשל: https://example.com/flowers.jpg</string>
|
||||
<string name="publish_dialog_attach_filename_placeholder">למשל: lilies.jpg</string>
|
||||
<string name="publish_dialog_phone_call_hint">שיחת טלפון</string>
|
||||
<string name="publish_dialog_phone_call_placeholder">למשל: +1234567890</string>
|
||||
<string name="message_bar_hint">נא למלא הודעה כאן</string>
|
||||
<string name="message_bar_publish_button_description">פרסום הודעה</string>
|
||||
<string name="message_bar_expand_button_description">אפשרויות נוספות</string>
|
||||
<string name="detail_fab_publish_description">התראת פרסום</string>
|
||||
<string name="settings_general_language_title">שפה</string>
|
||||
<string name="settings_general_language_summary_system">להשתמש בברירת המחדל של המערכת</string>
|
||||
<string name="settings_general_language_system_default">ברירת המחדל של המערכת</string>
|
||||
<string name="settings_general_dynamic_colors_title">צבעים דינמיים</string>
|
||||
<string name="custom_headers_dialog_button_delete">מחיקה</string>
|
||||
<string name="custom_headers_dialog_button_save">שמירה</string>
|
||||
<string name="custom_headers_dialog_button_add">הוספה</string>
|
||||
<string name="custom_headers_dialog_error_duplicate">כבר יש כותרת בשם הזה בשרת הזה</string>
|
||||
<string name="custom_headers_dialog_error_reserved_name">הכותרת הזאת שמורה ומוגדרת על ידי ntfy</string>
|
||||
<string name="custom_headers_dialog_error_invalid_name">שם הכותרת מכיל תווים שגויים</string>
|
||||
<string name="custom_headers_dialog_value_hint">ערך כותרת (למשל: 9f3c2e4a1b2d4e)</string>
|
||||
<string name="custom_headers_dialog_name_hint">שם כותרת (למשל: CF-Access-Client-Id)</string>
|
||||
<string name="custom_headers_dialog_base_url_hint">כתובת שירות</string>
|
||||
<string name="custom_headers_dialog_title_edit">עריכת כותרת מותאמת אישית</string>
|
||||
<string name="custom_headers_dialog_title_add">הוספת כותרת מותאמת אישית</string>
|
||||
<string name="settings_advanced_custom_headers_prefs_header_add_title">הוספת כותרת לשרת</string>
|
||||
<string name="settings_advanced_custom_headers_prefs_header_add">הוספת כותרת</string>
|
||||
<string name="settings_advanced_custom_headers_title">כותרות מותאמות אישית</string>
|
||||
<string name="settings_general_message_bar_summary_disabled">כפתור הפרסום מופיע בתחתית תצוגת הנושאים</string>
|
||||
<string name="settings_general_message_bar_summary_enabled">סרגל הודעות מופיע בתחתית תצוגת הנושאים</string>
|
||||
<string name="settings_general_message_bar_title">הצגת סרגל הודעות</string>
|
||||
<string name="settings_general_dynamic_colors_summary_enabled">להשתמש בצבעי המערכת הדינמיים</string>
|
||||
<string name="settings_general_dynamic_colors_summary_disabled">להשתמש בצבעי ערכת העיצוב של ntfy</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -347,4 +347,28 @@
|
|||
<string name="settings_general_dynamic_colors_title">Değişken renkler</string>
|
||||
<string name="settings_general_dynamic_colors_summary_enabled">Değişken sistem renkleri kullanılıyor</string>
|
||||
<string name="settings_general_dynamic_colors_summary_disabled">ntfy tema renkleri kullanılıyor</string>
|
||||
<string name="publish_dialog_title">%1$s\'a yayınla</string>
|
||||
<string name="publish_dialog_title_hint">Başlık</string>
|
||||
<string name="publish_dialog_title_placeholder">Örneğin, \'Kapıda birisi var\'</string>
|
||||
<string name="publish_dialog_message_hint">Mesaj</string>
|
||||
<string name="publish_dialog_tags_hint">Etiketler</string>
|
||||
<string name="publish_dialog_tags_placeholder">Orneğin, \'İkaz, Tehlike\'</string>
|
||||
<string name="publish_dialog_priority_hint">Öncelik</string>
|
||||
<string name="publish_dialog_button_publish">Yayınla</string>
|
||||
<string name="publish_dialog_error_sending">Mesaj: %1$s yayınlanamadı</string>
|
||||
<string name="publish_dialog_error_server">Mesaj: %1$s yayınlanamadı (kod %2$d)</string>
|
||||
<string name="publish_dialog_message_published">Mesaj yayınlandı</string>
|
||||
<string name="publish_dialog_uploading">Yüklenme: %1$s (%2$s / %3$s)</string>
|
||||
<string name="publish_dialog_upload_cancelled">Yüklenme iptal edildi</string>
|
||||
<string name="publish_dialog_chip_title">Başlık</string>
|
||||
<string name="publish_dialog_chip_tags">Etiketler</string>
|
||||
<string name="publish_dialog_chip_priority">Öncelik</string>
|
||||
<string name="publish_dialog_chip_click_url">Tıklama URL\'i</string>
|
||||
<string name="publish_dialog_chip_email">E-posta adresi</string>
|
||||
<string name="publish_dialog_chip_delay">Erteleme</string>
|
||||
<string name="publish_dialog_chip_markdown">\"Markdown\"</string>
|
||||
<string name="publish_dialog_chip_attach_url">URL\'le ekle</string>
|
||||
<string name="publish_dialog_chip_attach_file">Yerli dosya ekle</string>
|
||||
<string name="publish_dialog_chip_phone_call">Telefon görüşmesi</string>
|
||||
<string name="publish_dialog_click_url_hint">Tıklama URL\'i</string>
|
||||
</resources>
|
||||
|
|
|
|||
3
fastlane/metadata/android/en-US/changelog/NEXT.txt
Normal file
3
fastlane/metadata/android/en-US/changelog/NEXT.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Maintenance + bug fixes:
|
||||
* Hide "Exact alarms" setting if battery optimization exemption has been granted (#1456, thanks for reporting @HappyLer)
|
||||
* Fix ForegroundServiceDidNotStartInTimeException (#1520, attempt 3, d064e75)
|
||||
Loading…
Add table
Reference in a new issue