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">
</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) -->
<service

View file

@ -1,16 +1,10 @@
package io.heckel.ntfy.app
import android.app.Application
import android.content.Context
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.Log
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 = Repository.getInstance(applicationContext)
if (repository.getRecordLogs()) {

View file

@ -2,6 +2,7 @@ package io.heckel.ntfy.db
import android.content.Context
import android.content.SharedPreferences
import android.media.MediaPlayer
import android.os.Build
import androidx.annotation.WorkerThread
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 connectionStatesLiveData = MutableLiveData(connectionStates)
// TODO Move these into an ApplicationState singleton
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
val mediaPlayer = MediaPlayer()
init {
Log.d(TAG, "Created $this")
@ -288,6 +292,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
.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 {
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_DARK_MODE = "DarkMode"
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_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
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.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
@ -24,9 +23,9 @@ import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.*
import java.util.*
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val repository = Repository.getInstance(context)
fun display(subscription: Subscription, 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) {
val title = formatTitle(subscription, notification)
val channelId = toChannelId(notification.priority)
val insistent = notification.priority == 5 && repository.getInsistentMaxPriorityEnabled()
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, Colors.notificationIcon(context)))
@ -74,7 +74,8 @@ class NotificationService(val context: Context) {
.setAutoCancel(true) // Cancel when notification is clicked
setStyleAndText(builder, subscription, notification) // Preview picture or big text style
setClickAction(builder, subscription, notification)
maybeSetSound(builder, update)
maybeSetDeleteIntent(builder, insistent)
maybeSetSound(builder, insistent, update)
maybeSetProgress(builder, notification)
maybeAddOpenAction(builder, notification)
maybeAddBrowseAction(builder, notification)
@ -82,65 +83,24 @@ class NotificationService(val context: Context) {
maybeAddCancelAction(builder, notification)
maybeAddUserActions(builder, notification)
maybeCreateNotificationChannel(notification.priority)
val systemNotification = builder.build()
if (channelId == CHANNEL_ID_MAX) {
//systemNotification.flags = systemNotification.flags or android.app.Notification.FLAG_INSISTENT
}
notificationManager.notify(notification.notificationId, systemNotification)
maybePlayInsistentSound(insistent)
if (channelId == CHANNEL_ID_MAX) {
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()
notificationManager.notify(notification.notificationId, builder.build())
}
private fun maybeSetDeleteIntent(builder: NotificationCompat.Builder, insistent: Boolean) {
if (!insistent) {
return
}
val intent = Intent(context, DeleteBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.setDeleteIntent(pendingIntent)
}
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "AlarmReceiver.onReceive ${intent}")
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();
}
}
}
private fun maybeSetSound(builder: NotificationCompat.Builder, update: Boolean) {
if (!update) {
private fun maybeSetSound(builder: NotificationCompat.Builder, insistent: Boolean, update: Boolean) {
val hasSound = !update && !insistent
if (hasSound) {
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
builder.setSound(defaultSoundUri)
} 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? {
val intent = Intent(context, DetailActivity::class.java).apply {
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.
* .

View file

@ -297,6 +297,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
}
}
}
repository.mediaPlayer.stop()
}
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
val channelPrefsPrefId = context?.getString(R.string.settings_notifications_channel_prefs_key) ?: return
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_month">After one month</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_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>

View file

@ -18,6 +18,7 @@
<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_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_users_key" translatable="false">ManageUsers</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:entryValues="@array/settings_notifications_auto_delete_values"
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
app:key="@string/settings_notifications_channel_prefs_key"
app:title="@string/settings_notifications_channel_prefs_title"