This commit is contained in:
Philipp Heckel 2022-11-29 22:46:38 -05:00
parent e18be4a2a7
commit 55ad2e65b5
9 changed files with 98 additions and 62 deletions

View file

@ -134,7 +134,12 @@
android:exported="false"> android:exported="false">
</receiver> </receiver>
<receiver android:name=".msg.NotificationService$AlarmReceiver"/> <!-- Broadcast receiver for when the notification is swiped away (currently only to cancel the insistent sound) -->
<receiver
android:name=".msg.NotificationService$DeleteBroadcastReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<!-- Firebase messaging (note that this is empty in the F-Droid flavor) --> <!-- Firebase messaging (note that this is empty in the F-Droid flavor) -->
<service <service

View file

@ -1,16 +1,10 @@
package io.heckel.ntfy.app package io.heckel.ntfy.app
import android.app.Application import android.app.Application
import android.content.Context
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
class Application : Application() { class Application : Application() {
private val database by lazy {
Log.init(this) // What a hack, but this is super early and used everywhere
Database.getInstance(this)
}
val repository by lazy { val repository by lazy {
val repository = Repository.getInstance(applicationContext) val repository = Repository.getInstance(applicationContext)
if (repository.getRecordLogs()) { if (repository.getRecordLogs()) {

View file

@ -2,6 +2,7 @@ package io.heckel.ntfy.db
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.media.MediaPlayer
import android.os.Build import android.os.Build
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@ -18,7 +19,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
private val connectionStates = ConcurrentHashMap<Long, ConnectionState>() private val connectionStates = ConcurrentHashMap<Long, ConnectionState>()
private val connectionStatesLiveData = MutableLiveData(connectionStates) private val connectionStatesLiveData = MutableLiveData(connectionStates)
// TODO Move these into an ApplicationState singleton
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ... val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
val mediaPlayer = MediaPlayer()
init { init {
Log.d(TAG, "Created $this") Log.d(TAG, "Created $this")
@ -288,6 +292,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
.apply() .apply()
} }
fun getInsistentMaxPriorityEnabled(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED, false) // Disabled by default
}
fun setInsistentMaxPriorityEnabled(enabled: Boolean) {
sharedPrefs.edit()
.putBoolean(SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED, enabled)
.apply()
}
fun getRecordLogs(): Boolean { fun getRecordLogs(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_RECORD_LOGS_ENABLED, false) // Disabled by default return sharedPrefs.getBoolean(SHARED_PREFS_RECORD_LOGS_ENABLED, false) // Disabled by default
} }
@ -459,6 +473,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol" const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol"
const val SHARED_PREFS_DARK_MODE = "DarkMode" const val SHARED_PREFS_DARK_MODE = "DarkMode"
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
const val SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED = "InsistentMaxPriority"
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs" const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime" 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_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)

View file

@ -7,7 +7,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaPlayer
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -24,9 +23,9 @@ import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import java.util.* import java.util.*
class NotificationService(val context: Context) { class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val repository = Repository.getInstance(context)
fun display(subscription: Subscription, notification: Notification) { fun display(subscription: Subscription, notification: Notification) {
Log.d(TAG, "Displaying notification $notification") Log.d(TAG, "Displaying notification $notification")
@ -66,6 +65,7 @@ class NotificationService(val context: Context) {
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) { private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) {
val title = formatTitle(subscription, notification) val title = formatTitle(subscription, notification)
val channelId = toChannelId(notification.priority) val channelId = toChannelId(notification.priority)
val insistent = notification.priority == 5 && repository.getInsistentMaxPriorityEnabled()
val builder = NotificationCompat.Builder(context, channelId) val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, Colors.notificationIcon(context))) .setColor(ContextCompat.getColor(context, Colors.notificationIcon(context)))
@ -74,7 +74,8 @@ class NotificationService(val context: Context) {
.setAutoCancel(true) // Cancel when notification is clicked .setAutoCancel(true) // Cancel when notification is clicked
setStyleAndText(builder, subscription, notification) // Preview picture or big text style setStyleAndText(builder, subscription, notification) // Preview picture or big text style
setClickAction(builder, subscription, notification) setClickAction(builder, subscription, notification)
maybeSetSound(builder, update) maybeSetDeleteIntent(builder, insistent)
maybeSetSound(builder, insistent, update)
maybeSetProgress(builder, notification) maybeSetProgress(builder, notification)
maybeAddOpenAction(builder, notification) maybeAddOpenAction(builder, notification)
maybeAddBrowseAction(builder, notification) maybeAddBrowseAction(builder, notification)
@ -82,65 +83,24 @@ class NotificationService(val context: Context) {
maybeAddCancelAction(builder, notification) maybeAddCancelAction(builder, notification)
maybeAddUserActions(builder, notification) maybeAddUserActions(builder, notification)
maybeCreateNotificationChannel(notification.priority) maybeCreateNotificationChannel(notification.priority)
val systemNotification = builder.build() maybePlayInsistentSound(insistent)
if (channelId == CHANNEL_ID_MAX) {
//systemNotification.flags = systemNotification.flags or android.app.Notification.FLAG_INSISTENT
}
notificationManager.notify(notification.notificationId, systemNotification)
if (channelId == CHANNEL_ID_MAX) { notificationManager.notify(notification.notificationId, builder.build())
Log.d(TAG, "Setting alarm")
/*val calendar = Calendar.getInstance()
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val intent = Intent(context, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, 1111, intent, PendingIntent.FLAG_IMMUTABLE)
// when using setAlarmClock() it displays a notification until alarm rings and when pressed it takes us to mainActivity
alarmManager?.set(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis, pendingIntent
)*/
val alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val mMediaPlayer = MediaPlayer()
mMediaPlayer.setDataSource(context, alert)
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
mMediaPlayer.setAudioAttributes(AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build())
mMediaPlayer.isLooping = true;
mMediaPlayer.prepare();
mMediaPlayer.start();
mMediaPlayer.stop()
}
}
} }
class AlarmReceiver : BroadcastReceiver() { private fun maybeSetDeleteIntent(builder: NotificationCompat.Builder, insistent: Boolean) {
override fun onReceive(context: Context?, intent: Intent?) { if (!insistent) {
Log.d(TAG, "AlarmReceiver.onReceive ${intent}") return
val context = context ?: return
val alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val mMediaPlayer = MediaPlayer()
mMediaPlayer.setDataSource(context, alert)
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
mMediaPlayer.setLooping(true);
mMediaPlayer.prepare();
mMediaPlayer.start();
}
} }
val intent = Intent(context, DeleteBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.setDeleteIntent(pendingIntent)
} }
private fun maybeSetSound(builder: NotificationCompat.Builder, update: Boolean) { private fun maybeSetSound(builder: NotificationCompat.Builder, insistent: Boolean, update: Boolean) {
if (!update) { val hasSound = !update && !insistent
if (hasSound) {
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
builder.setSound(defaultSoundUri) builder.setSound(defaultSoundUri)
} else { } else {
@ -353,6 +313,17 @@ class NotificationService(val context: Context) {
} }
} }
/**
* Receives a broadcast when a notification is swiped away. This is currently
* only called for notifications with an insistent sound.
*/
class DeleteBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val mediaPlayer = Repository.getInstance(context).mediaPlayer
mediaPlayer.stop()
}
}
private fun detailActivityIntent(subscription: Subscription): PendingIntent? { private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
val intent = Intent(context, DetailActivity::class.java).apply { val intent = Intent(context, DetailActivity::class.java).apply {
putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
@ -416,6 +387,28 @@ class NotificationService(val context: Context) {
} }
} }
private fun maybePlayInsistentSound(insistent: Boolean) {
if (!insistent) {
return
}
try {
Log.d(TAG, "Playing insistent alarm")
val mediaPlayer = repository.mediaPlayer
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
mediaPlayer.reset()
mediaPlayer.setDataSource(context, alert)
mediaPlayer.setAudioAttributes(AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build())
mediaPlayer.isLooping = true;
mediaPlayer.prepare()
mediaPlayer.start()
}
} catch (e: Exception) {
Log.w(TAG, "Failed playing insistent alarm", e)
}
}
/** /**
* Activity used to launch a URL. * Activity used to launch a URL.
* . * .

View file

@ -297,6 +297,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
} }
} }
} }
repository.mediaPlayer.stop()
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View file

@ -200,6 +200,26 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
} }
} }
// Keep alerting for max priority
val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return
val insistentMaxPriority: SwitchPreference? = findPreference(insistentMaxPriorityPrefId)
insistentMaxPriority?.isChecked = repository.getInsistentMaxPriorityEnabled()
insistentMaxPriority?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setInsistentMaxPriorityEnabled(value)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getInsistentMaxPriorityEnabled()
}
}
insistentMaxPriority?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_notifications_insistent_max_priority_summary_enabled)
} else {
getString(R.string.settings_notifications_insistent_max_priority_summary_disabled)
}
}
// Channel settings // Channel settings
val channelPrefsPrefId = context?.getString(R.string.settings_notifications_channel_prefs_key) ?: return val channelPrefsPrefId = context?.getString(R.string.settings_notifications_channel_prefs_key) ?: return
val channelPrefs: Preference? = findPreference(channelPrefsPrefId) val channelPrefs: Preference? = findPreference(channelPrefsPrefId)

View file

@ -279,6 +279,9 @@
<string name="settings_notifications_auto_delete_one_week">After one week</string> <string name="settings_notifications_auto_delete_one_week">After one week</string>
<string name="settings_notifications_auto_delete_one_month">After one month</string> <string name="settings_notifications_auto_delete_one_month">After one month</string>
<string name="settings_notifications_auto_delete_three_months">After 3 months</string> <string name="settings_notifications_auto_delete_three_months">After 3 months</string>
<string name="settings_notifications_insistent_max_priority_title">Keep alerting for highest priority</string>
<string name="settings_notifications_insistent_max_priority_summary_enabled">Max priority notifications continuously play the notification sound until dismissed. This overrides Do Not Disturb mode.</string>
<string name="settings_notifications_insistent_max_priority_summary_disabled">Max priority notifications alert only once. If enabled, the notification sound will repeat and override Do Not Disturb mode.</string>
<string name="settings_general_header">General</string> <string name="settings_general_header">General</string>
<string name="settings_general_default_base_url_title">Default server</string> <string name="settings_general_default_base_url_title">Default server</string>
<string name="settings_general_default_base_url_message">Enter your server\'s root URL to use your own server as a default when subscribing to new topics and/or sharing to topics.</string> <string name="settings_general_default_base_url_message">Enter your server\'s root URL to use your own server as a default when subscribing to new topics and/or sharing to topics.</string>

View file

@ -18,6 +18,7 @@
<string name="settings_notifications_channel_prefs_key" translatable="false">ChannelPrefs</string> <string name="settings_notifications_channel_prefs_key" translatable="false">ChannelPrefs</string>
<string name="settings_notifications_auto_download_key" translatable="false">AutoDownload</string> <string name="settings_notifications_auto_download_key" translatable="false">AutoDownload</string>
<string name="settings_notifications_auto_delete_key" translatable="false">AutoDelete</string> <string name="settings_notifications_auto_delete_key" translatable="false">AutoDelete</string>
<string name="settings_notifications_insistent_max_priority_key" translatable="false">InsistentMaxPriority</string>
<string name="settings_general_default_base_url_key" translatable="false">DefaultBaseURL</string> <string name="settings_general_default_base_url_key" translatable="false">DefaultBaseURL</string>
<string name="settings_general_users_key" translatable="false">ManageUsers</string> <string name="settings_general_users_key" translatable="false">ManageUsers</string>
<string name="settings_general_dark_mode_key" translatable="false">DarkMode</string> <string name="settings_general_dark_mode_key" translatable="false">DarkMode</string>

View file

@ -25,6 +25,10 @@
app:entries="@array/settings_notifications_auto_delete_entries" app:entries="@array/settings_notifications_auto_delete_entries"
app:entryValues="@array/settings_notifications_auto_delete_values" app:entryValues="@array/settings_notifications_auto_delete_values"
app:defaultValue="2592000"/> app:defaultValue="2592000"/>
<SwitchPreference
app:key="@string/settings_notifications_insistent_max_priority_key"
app:title="@string/settings_notifications_insistent_max_priority_title"
app:defaultValue="false"/>
<Preference <Preference
app:key="@string/settings_notifications_channel_prefs_key" app:key="@string/settings_notifications_channel_prefs_key"
app:title="@string/settings_notifications_channel_prefs_title" app:title="@string/settings_notifications_channel_prefs_title"