From bd2dd2adf20607e121e5d7722551381ee977f45d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:56:23 -0500 Subject: [PATCH] feat: Add support for conversation shortcuts on Android 11+ This commit introduces the functionality to create and manage conversation shortcuts on Android devices running Android 11 (API level 30) and above. These shortcuts provide quick access to ongoing conversations directly from the launcher. Key changes include: - Upon opening a conversation in the app, a LocusId is set for the activity, enabling the system to associate the activity with a specific conversation shortcut. - When a new message notification is received, the app now generates or updates a dynamic conversation shortcut using the ShortcutManagerCompat API. This shortcut includes relevant information such as the contact's name and an icon. - The notification itself is enhanced to include the shortcut ID and LocusId, further integrating it with the conversation shortcuts feature. --- .../mesh/service/MeshServiceNotifications.kt | 57 ++++++++++++++++++- .../com/geeksville/mesh/ui/message/Message.kt | 7 ++- 2 files changed, 62 insertions(+), 2 deletions(-) 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 adb4c2d7..09efc0ce 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt @@ -24,6 +24,7 @@ import android.app.PendingIntent import android.content.ContentResolver import android.content.Context import android.content.Intent +import android.content.pm.ShortcutInfo import android.graphics.Color import android.media.AudioAttributes import android.media.RingtoneManager @@ -31,6 +32,10 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.Person +import androidx.core.content.LocusIdCompat +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R @@ -314,11 +319,15 @@ class MeshServiceNotifications( ) } - fun updateMessageNotification(contactKey: String, name: String, message: String) = + fun updateMessageNotification(contactKey: String, name: String, message: String) { notificationManager.notify( contactKey.hashCode(), // show unique notifications, createMessageNotification(contactKey, name, message) ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + updateConversationShortcuts(contactKey, name) + } + } fun showAlertNotification(contactKey: String, name: String, alert: String) { notificationManager.notify( @@ -426,6 +435,45 @@ class MeshServiceNotifications( return serviceNotificationBuilder.build() } + @RequiresApi(Build.VERSION_CODES.Q) + private fun updateConversationShortcuts( + contactKey: String, + name: String, + ) { + val shortcutInfo = createShortcutInfo(context, contactKey, name) + ShortcutManagerCompat.pushDynamicShortcut(context, shortcutInfo) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun createShortcutInfo( + context: Context, + contactKey: String, + name: String, + ): ShortcutInfoCompat { + val shortcutIntent = Intent(context, MainActivity::class.java).apply { + action = OPEN_MESSAGE_ACTION + putExtra(OPEN_MESSAGE_EXTRA_CONTACT_KEY, contactKey) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val shortcutId = "conv_$contactKey" + return ShortcutInfoCompat.Builder(context, "conv_$contactKey") + .setShortLabel(name) + .setLongLabel("Chat with $name") + .setIcon( + IconCompat.createWithResource( + context, + R.drawable.app_icon + ) + ) // Replace with contact icon if available + .setIntent(shortcutIntent) + .setCategories(setOf(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION)) + .setPerson(Person.Builder().setName(name).build()) + .setLongLived(true) + .setLocusId(LocusIdCompat(shortcutId)) + .build() + } + + // Modify updateMessageNotification to create the shortcut lateinit var messageNotificationBuilder: NotificationCompat.Builder private fun createMessageNotification( contactKey: String, @@ -435,12 +483,19 @@ class MeshServiceNotifications( if (!::messageNotificationBuilder.isInitialized) { messageNotificationBuilder = commonBuilder(messageChannelId) } + val shortcutId = "conv_$contactKey" val person = Person.Builder().setName(name).build() with(messageNotificationBuilder) { setContentIntent(openMessageIntent(contactKey)) priority = NotificationCompat.PRIORITY_DEFAULT setCategory(Notification.CATEGORY_MESSAGE) setAutoCancel(true) + setShortcutId(shortcutId) + setLocusId(LocusIdCompat(shortcutId)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val shortcutInfo = createShortcutInfo(context, contactKey, name) + setShortcutInfo(shortcutInfo) + } setStyle( NotificationCompat.MessagingStyle(person) .addMessage(message, System.currentTimeMillis(), person) diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt index 271ba71f..c1fb9887 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/Message.kt @@ -17,6 +17,8 @@ package com.geeksville.mesh.ui.message +import android.content.LocusId +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -137,7 +139,10 @@ class MessagesFragment : Fragment(), Logging { ): View { val contactKey = arguments?.getString("contactKey").toString() val message = arguments?.getString("message").toString() - + val shortcutId = "conv_$contactKey" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requireActivity().setLocusContext(LocusId(shortcutId), null) + } return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent {