Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Show ALERT_APP notifications and override silent mode #1515

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@
<!-- zxing library for QR Code scanning using camera -->
<uses-permission android:name="android.permission.CAMERA" />

<!-- Needed for AlertApp to override Do Not Disturb -->
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />

<uses-feature
android:name="android.hardware.camera"
android:required="false" />
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/geeksville/mesh/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion app/src/main/java/com/geeksville/mesh/android/ContextServices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -167,12 +168,21 @@ fun Context.hasLocationPermission() = getLocationPermissions().isEmpty()
*/
fun Context.getNotificationPermissions(): Array<String> {
val perms = mutableListOf<String>()
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
}
20 changes: 19 additions & 1 deletion app/src/main/java/com/geeksville/mesh/service/MeshService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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!!
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,23 +42,77 @@ 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(
private val context: Context
) {

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

// 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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
<string name="message_reception_state">message reception state</string>
<string name="message_delivery_status">Message delivery status</string>
<string name="meshtastic_messages_notifications">Message notifications</string>
<string name="meshtastic_alerts_notifications">Alert notifications</string>
<string name="protocol_stress_test">Protocol stress test</string>
<string name="firmware_too_old">Firmware update required</string>
<string name="firmware_old">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 <a href="https://meshtastic.org/docs/getting-started/flashing-firmware">our Firmware Installation guide</a>.</string>
Expand Down Expand Up @@ -314,4 +315,6 @@
<string name="unknown_age">Unknown Age</string>
<string name="copy">Copy</string>
<string name="alert_bell_text">Alert Bell Character!</string>
<string name="dnd_required">Do Not Disturb Permission Required</string>
<string name="why_dnd_required"><![CDATA[\"Alerts requires Mode Control permission to work\\n To grant Modes Control permission, tap on \'Open settings\' > Allow Modes access to \'Meshtastic\' app\"]]></string>
</resources>
Loading