kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: Allow direct message replies from notifications (#1994)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>pull/1997/head
rodzic
6915249121
commit
4d6eb3dfe9
|
@ -1,20 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (c) 2025 Meshtastic LLC
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
~ Copyright (c) 2025 Meshtastic LLC
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
@ -223,6 +223,7 @@
|
|||
android:path="com.geeksville.mesh" /> -->
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="com.geeksville.mesh.service.ReplyReceiver"/>
|
||||
|
||||
<activity
|
||||
android:name=".AppIntroduction"/>
|
||||
|
|
|
@ -32,6 +32,7 @@ import android.os.Build
|
|||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.net.toUri
|
||||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.R
|
||||
|
@ -39,6 +40,7 @@ import com.geeksville.mesh.TelemetryProtos.LocalStats
|
|||
import com.geeksville.mesh.android.notificationManager
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.navigation.DEEP_LINK_BASE_URI
|
||||
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
|
||||
import com.geeksville.mesh.util.formatUptime
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
@ -314,6 +316,10 @@ class MeshServiceNotifications(
|
|||
)
|
||||
}
|
||||
|
||||
fun cancelMessageNotification(contactKey: String) {
|
||||
notificationManager.cancel(contactKey.hashCode())
|
||||
}
|
||||
|
||||
fun updateMessageNotification(contactKey: String, name: String, message: String) =
|
||||
notificationManager.notify(
|
||||
contactKey.hashCode(), // show unique notifications,
|
||||
|
@ -354,6 +360,13 @@ class MeshServiceNotifications(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createMessageReplyIntent(contactKey: String): Intent {
|
||||
return Intent(context, ReplyReceiver::class.java).apply {
|
||||
action = ReplyReceiver.REPLY_ACTION
|
||||
putExtra(ReplyReceiver.CONTACT_KEY, contactKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOpenMessageIntent(contactKey: String): PendingIntent {
|
||||
val deepLink = "$DEEP_LINK_BASE_URI/messages/$contactKey"
|
||||
val deepLinkIntent = Intent(
|
||||
|
@ -365,7 +378,7 @@ class MeshServiceNotifications(
|
|||
|
||||
val deepLinkPendingIntent: PendingIntent = TaskStackBuilder.create(context).run {
|
||||
addNextIntentWithParentStack(deepLinkIntent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
getPendingIntent(0, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
return deepLinkPendingIntent
|
||||
|
@ -376,6 +389,7 @@ class MeshServiceNotifications(
|
|||
contentIntent: PendingIntent? = null
|
||||
): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(context, channel)
|
||||
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(contentIntent ?: openAppIntent)
|
||||
|
||||
|
@ -425,17 +439,37 @@ class MeshServiceNotifications(
|
|||
return serviceNotificationBuilder.build()
|
||||
}
|
||||
|
||||
lateinit var messageNotificationBuilder: NotificationCompat.Builder
|
||||
private fun createMessageNotification(
|
||||
contactKey: String,
|
||||
name: String,
|
||||
message: String
|
||||
): Notification {
|
||||
if (!::messageNotificationBuilder.isInitialized) {
|
||||
messageNotificationBuilder =
|
||||
commonBuilder(messageChannelId, createOpenMessageIntent(contactKey))
|
||||
}
|
||||
val messageNotificationBuilder: NotificationCompat.Builder =
|
||||
commonBuilder(messageChannelId, createOpenMessageIntent(contactKey))
|
||||
|
||||
val person = Person.Builder().setName(name).build()
|
||||
// Key for the string that's delivered in the action's intent.
|
||||
val replyLabel: String = context.getString(R.string.reply)
|
||||
val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run {
|
||||
setLabel(replyLabel)
|
||||
build()
|
||||
}
|
||||
|
||||
// Build a PendingIntent for the reply action to trigger.
|
||||
val replyPendingIntent: PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
contactKey.hashCode(),
|
||||
createMessageReplyIntent(contactKey),
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
// Create the reply action and add the remote input.
|
||||
val action: NotificationCompat.Action = NotificationCompat.Action.Builder(
|
||||
android.R.drawable.ic_menu_send,
|
||||
replyLabel,
|
||||
replyPendingIntent
|
||||
).addRemoteInput(remoteInput).build()
|
||||
|
||||
with(messageNotificationBuilder) {
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setCategory(Notification.CATEGORY_MESSAGE)
|
||||
|
@ -444,6 +478,7 @@ class MeshServiceNotifications(
|
|||
NotificationCompat.MessagingStyle(person)
|
||||
.addMessage(message, System.currentTimeMillis(), person)
|
||||
)
|
||||
addAction(action)
|
||||
setWhen(System.currentTimeMillis())
|
||||
setShowWhen(true)
|
||||
}
|
||||
|
@ -457,11 +492,11 @@ class MeshServiceNotifications(
|
|||
alert: String
|
||||
): Notification {
|
||||
if (!::alertNotificationBuilder.isInitialized) {
|
||||
alertNotificationBuilder = commonBuilder(alertChannelId)
|
||||
alertNotificationBuilder =
|
||||
commonBuilder(alertChannelId, createOpenMessageIntent(contactKey))
|
||||
}
|
||||
val person = Person.Builder().setName(name).build()
|
||||
with(alertNotificationBuilder) {
|
||||
setContentIntent(createOpenMessageIntent(contactKey))
|
||||
priority = NotificationCompat.PRIORITY_HIGH
|
||||
setCategory(Notification.CATEGORY_ALARM)
|
||||
setAutoCancel(true)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import androidx.core.app.RemoteInput
|
||||
import com.geeksville.mesh.DataPacket
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import jakarta.inject.Inject
|
||||
|
||||
/**
|
||||
* A [BroadcastReceiver] that handles inline replies from notifications.
|
||||
*
|
||||
* This receiver is triggered when a user replies to a message directly from a notification.
|
||||
* It extracts the reply text and the contact key from the intent, sends the message
|
||||
* using the [ServiceRepository], and then cancels the original notification.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ReplyReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var serviceRepository: ServiceRepository
|
||||
|
||||
companion object {
|
||||
const val REPLY_ACTION = "com.geeksville.mesh.REPLY_ACTION"
|
||||
const val CONTACT_KEY = "contactKey"
|
||||
const val KEY_TEXT_REPLY = "key_text_reply"
|
||||
}
|
||||
|
||||
private fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
|
||||
// contactKey: unique contact key filter (channel)+(nodeId)
|
||||
val channel = contactKey[0].digitToIntOrNull()
|
||||
val dest = if (channel != null) contactKey.substring(1) else contactKey
|
||||
val p = DataPacket(dest, channel ?: 0, str)
|
||||
serviceRepository.meshService?.send(p)
|
||||
}
|
||||
|
||||
override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||||
val remoteInput = RemoteInput.getResultsFromIntent(intent)
|
||||
|
||||
if (remoteInput != null) {
|
||||
val contactKey = intent.getStringExtra(CONTACT_KEY) ?: ""
|
||||
val message = remoteInput.getCharSequence(
|
||||
KEY_TEXT_REPLY
|
||||
)?.toString() ?: ""
|
||||
sendMessage(message, contactKey)
|
||||
MeshServiceNotifications(context).cancelMessageNotification(contactKey)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -660,4 +660,5 @@
|
|||
<string name="map">Map</string>
|
||||
<string name="contacts">Contacts</string>
|
||||
<string name="nodes">Nodes</string>
|
||||
<string name="reply">Reply</string>
|
||||
</resources>
|
||||
|
|
Ładowanie…
Reference in New Issue