diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0f9e42f02..89215e377 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -73,6 +73,9 @@
+
+
+
diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
index 3b70b5c61..84e4fe59a 100644
--- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt
@@ -25,10 +25,12 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.hardware.usb.UsbManager
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
+import android.provider.Settings
import android.text.method.LinkMovementMethod
import android.view.Menu
import android.view.MenuItem
@@ -56,6 +58,7 @@ import com.geeksville.mesh.android.ServiceClient
import com.geeksville.mesh.android.getBluetoothPermissions
import com.geeksville.mesh.android.getNotificationPermissions
import com.geeksville.mesh.android.hasBluetoothPermission
+import com.geeksville.mesh.android.isNotificationPolicyAccessGranted
import com.geeksville.mesh.android.hasNotificationPermission
import com.geeksville.mesh.android.permissionMissing
import com.geeksville.mesh.android.rationaleDialog
@@ -437,6 +440,15 @@ class MainActivity : AppCompatActivity(), Logging {
notificationPermissionsLauncher.launch(notificationPermissions)
}
}
+ if (!isNotificationPolicyAccessGranted() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ rationaleDialog(
+ title = R.string.dnd_required,
+ rationale = getString(R.string.why_dnd_required),
+ ) {
+ val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)
+ startActivity(intent)
+ }
+ }
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
index 473facb08..6aefb9ad7 100644
--- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
+++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
@@ -24,6 +24,7 @@ import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
+import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
@@ -167,12 +168,21 @@ fun Context.hasLocationPermission() = getLocationPermissions().isEmpty()
*/
fun Context.getNotificationPermissions(): Array {
val perms = mutableListOf()
- if (android.os.Build.VERSION.SDK_INT >= 33) {
+ if (Build.VERSION.SDK_INT >= 33) {
perms.add(Manifest.permission.POST_NOTIFICATIONS)
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ perms.add(Manifest.permission.ACCESS_NOTIFICATION_POLICY)
+ }
return getMissingPermissions(perms)
}
/** @return true if the user already has notification permission */
fun Context.hasNotificationPermission() = getNotificationPermissions().isEmpty()
+
+fun Context.isNotificationPolicyAccessGranted() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ notificationManager.isNotificationPolicyAccessGranted
+} else {
+ false
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index f88f23d64..abbd0f1c0 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -271,6 +271,14 @@ class MeshService : Service(), Logging {
startPacketQueue()
}
+ private fun showAlertNotification(contactKey: String, dataPacket: DataPacket) {
+ serviceNotifications.showAlertNotification(
+ contactKey,
+ getSenderName(dataPacket),
+ dataPacket.text!!
+ )
+ }
+
private fun updateMessageNotification(contactKey: String, dataPacket: DataPacket) {
val message: String = when (dataPacket.dataType) {
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> dataPacket.text!!
@@ -630,6 +638,7 @@ class MeshService : Service(), Logging {
private val rememberDataType = setOf(
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
+ Portnums.PortNum.ALERT_APP_VALUE,
Portnums.PortNum.WAYPOINT_APP_VALUE,
)
@@ -666,7 +675,11 @@ class MeshService : Service(), Logging {
packetRepository.get().apply {
insert(packetToSave)
val isMuted = getContactSettings(contactKey).isMuted
- if (updateNotification && !isMuted) updateMessageNotification(contactKey, dataPacket)
+ if (packetToSave.port_num == Portnums.PortNum.ALERT_APP_VALUE && !isMuted) {
+ showAlertNotification(contactKey, dataPacket)
+ } else if (updateNotification && !isMuted) {
+ updateMessageNotification(contactKey, dataPacket)
+ }
}
}
}
@@ -704,6 +717,11 @@ class MeshService : Service(), Logging {
}
}
+ Portnums.PortNum.ALERT_APP_VALUE -> {
+ debug("Received ALERT_APP from $fromId")
+ rememberDataPacket(dataPacket)
+ }
+
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
val u = MeshProtos.Waypoint.parseFrom(data.payload)
// Validate locked Waypoints from the original sender
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
index 554ad0262..8cb9898f7 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
@@ -22,11 +22,17 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
+import android.content.Context.AUDIO_SERVICE
import android.content.Intent
import android.graphics.Color
import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.MediaMetadataRetriever
import android.media.RingtoneManager
+import android.net.Uri
import android.os.Build
+import android.os.Handler
+import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
@@ -36,6 +42,7 @@ import com.geeksville.mesh.TelemetryProtos.LocalStats
import com.geeksville.mesh.android.notificationManager
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.util.formatUptime
+import kotlin.math.ceil
@Suppress("TooManyFunctions")
class MeshServiceNotifications(
@@ -43,9 +50,11 @@ class MeshServiceNotifications(
) {
companion object {
+ private const val OVERRIDE_VOLUME = 8.8f
private const val FIFTEEN_MINUTES_IN_MILLIS = 15L * 60 * 1000
const val OPEN_MESSAGE_ACTION = "com.geeksville.mesh.OPEN_MESSAGE_ACTION"
- const val OPEN_MESSAGE_EXTRA_CONTACT_KEY = "com.geeksville.mesh.OPEN_MESSAGE_EXTRA_CONTACT_KEY"
+ const val OPEN_MESSAGE_EXTRA_CONTACT_KEY =
+ "com.geeksville.mesh.OPEN_MESSAGE_EXTRA_CONTACT_KEY"
}
private val notificationManager: NotificationManager get() = context.notificationManager
@@ -53,6 +62,57 @@ class MeshServiceNotifications(
// We have two notification channels: one for general service status and another one for messages
val notifyId = 101
+ private fun overrideSilentModeAndConfigureCustomVolume(ringToneVolume: Float?) {
+ val audioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
+ var originalRingMode = audioManager.ringerMode
+ val originalNotificationVolume =
+ audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION)
+ val maxNotificationVolume =
+ audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION)
+
+ // When DND mode is enabled, we get ringerMode as silent even though actual ringer mode is Normal
+ val isDndModeEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ notificationManager.currentInterruptionFilter != NotificationManager.INTERRUPTION_FILTER_ALL
+ } else {
+ true
+ }
+ if (isDndModeEnabled &&
+ originalRingMode == AudioManager.RINGER_MODE_SILENT &&
+ originalNotificationVolume != 0
+ ) {
+ originalRingMode = AudioManager.RINGER_MODE_NORMAL
+ }
+
+ val newVolume = if (ringToneVolume != null) {
+ ceil(maxNotificationVolume * ringToneVolume).toInt()
+ } else {
+ originalNotificationVolume
+ }
+
+ audioManager.ringerMode = AudioManager.RINGER_MODE_NORMAL
+ audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, newVolume, 0)
+
+ // Resetting the original ring mode, volume and dnd mode
+ Handler(Looper.getMainLooper()).postDelayed(
+ {
+ audioManager.ringerMode = originalRingMode
+ audioManager.setStreamVolume(
+ AudioManager.STREAM_NOTIFICATION,
+ originalNotificationVolume,
+ 0
+ )
+ },
+ getSoundFileDuration(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+ )
+ }
+
+ private fun getSoundFileDuration(uri: Uri): Long {
+ val mmr = MediaMetadataRetriever()
+ mmr.setDataSource(context, uri)
+ val durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
+ return durationStr?.toLong() ?: 0
+ }
+
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = "my_service"
@@ -93,6 +153,31 @@ class MeshServiceNotifications(
return channelId
}
+ @RequiresApi(Build.VERSION_CODES.O)
+ private fun createAlertNotificationChannel(): String {
+ val channelId = "my_alerts"
+ val channelName = context.getString(R.string.meshtastic_alerts_notifications)
+ val channel = NotificationChannel(
+ channelId,
+ channelName,
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
+ setBypassDnd(true)
+ lightColor = Color.BLUE
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ setShowBadge(true)
+ setSound(
+ RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
+ AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build()
+ )
+ }
+ notificationManager.createNotificationChannel(channel)
+ return channelId
+ }
+
@RequiresApi(Build.VERSION_CODES.O)
private fun createNewNodeNotificationChannel(): String {
val channelId = "new_nodes"
@@ -137,6 +222,14 @@ class MeshServiceNotifications(
}
}
+ private val alertChannelId: String by lazy {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createAlertNotificationChannel()
+ } else {
+ ""
+ }
+ }
+
private val newNodeChannelId: String by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNewNodeNotificationChannel()
@@ -179,6 +272,14 @@ class MeshServiceNotifications(
createMessageNotification(contactKey, name, message)
)
+ fun showAlertNotification(contactKey: String, name: String, alert: String) {
+ overrideSilentModeAndConfigureCustomVolume(OVERRIDE_VOLUME)
+ notificationManager.notify(
+ contactKey.hashCode(), // show unique notifications,
+ createAlertNotification(contactKey, name, alert)
+ )
+ }
+
fun showNewNodeSeenNotification(node: NodeEntity) {
notificationManager.notify(
node.num, // show unique notifications
@@ -268,7 +369,11 @@ class MeshServiceNotifications(
}
lateinit var messageNotificationBuilder: NotificationCompat.Builder
- private fun createMessageNotification(contactKey: String, name: String, message: String): Notification {
+ private fun createMessageNotification(
+ contactKey: String,
+ name: String,
+ message: String
+ ): Notification {
if (!::messageNotificationBuilder.isInitialized) {
messageNotificationBuilder = commonBuilder(messageChannelId)
}
@@ -279,12 +384,36 @@ class MeshServiceNotifications(
setCategory(Notification.CATEGORY_MESSAGE)
setAutoCancel(true)
setStyle(
- NotificationCompat.MessagingStyle(person).addMessage(message, System.currentTimeMillis(), person)
+ NotificationCompat.MessagingStyle(person)
+ .addMessage(message, System.currentTimeMillis(), person)
)
}
return messageNotificationBuilder.build()
}
+ lateinit var alertNotificationBuilder: NotificationCompat.Builder
+ private fun createAlertNotification(
+ contactKey: String,
+ name: String,
+ alert: String
+ ): Notification {
+ if (!::alertNotificationBuilder.isInitialized) {
+ alertNotificationBuilder = commonBuilder(alertChannelId)
+ }
+ val person = Person.Builder().setName(name).build()
+ with(alertNotificationBuilder) {
+ setContentIntent(openMessageIntent(contactKey))
+ priority = NotificationCompat.PRIORITY_MAX
+ setCategory(Notification.CATEGORY_MESSAGE)
+ setAutoCancel(true)
+ setStyle(
+ NotificationCompat.MessagingStyle(person)
+ .addMessage(alert, System.currentTimeMillis(), person)
+ )
+ }
+ return alertNotificationBuilder.build()
+ }
+
lateinit var newNodeSeenNotificationBuilder: NotificationCompat.Builder
private fun createNewNodeSeenNotification(name: String, message: String? = null): Notification {
if (!::newNodeSeenNotificationBuilder.isInitialized) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63975f30e..4df3071cc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -141,6 +141,7 @@
message reception state
Message delivery status
Message notifications
+ Alert notifications
Protocol stress test
Firmware update required
The radio firmware is too old to talk to this application, please go to the settings pane and choose \"Update Firmware\". For more information on this see our Firmware Installation guide.
@@ -314,4 +315,6 @@
Unknown Age
Copy
Alert Bell Character!
+ Do Not Disturb Permission Required
+ Allow Modes access to \'Meshtastic\' app\"]]>