Offer conversations in the system share sheet

Sharing from another app listed Element X once, with no way to pick a
conversation: the Direct Share row never showed any.

The app already publishes a long-lived, conversation-categorised shortcut per
room, which is most of what the sharing shortcuts API needs. The part that was
missing is the declaration that ties those shortcuts to the share intent: the
launcher activity had no `android.app.shortcuts` meta-data, so there was no
`<share-target>` for the system to match them against.

It now ships one, matching the activity that already receives ACTION_SEND, and
the shortcuts carry the category it names.

The system offers these from Android 10 onwards, where sharing shortcuts are
handled natively. Reaching further back would additionally need the
androidx.sharetarget library, which is not added here.

Part of #3835
This commit is contained in:
hayaksi1
2026-08-26 03:18:28 +03:00
parent a2d3488426
commit 6d28d91541
4 changed files with 54 additions and 1 deletions
+6
View File
@@ -139,6 +139,12 @@
<data android:host="user" />
<data android:host="room" />
</intent-filter>
<!--
Conversation shortcuts offered in the Direct Share row of the system share sheet.
-->
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<!-- Incoming share simple -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
Please see LICENSE files in the repository root for full details.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Lets the system offer the conversation shortcuts published by
DefaultNotificationConversationService in the Direct Share row of the share sheet.
The category must match SHARE_TARGET_CATEGORY in that class.
-->
<share-target android:targetClass="io.element.android.x.MainActivity">
<data android:mimeType="*/*" />
<category android:name="io.element.android.category.SHARE_TARGET" />
</share-target>
</shortcuts>
@@ -42,6 +42,12 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
/**
* Category shared with the `<share-target>` in `app/src/main/res/xml/shortcuts.xml`. The system only offers a shortcut
* in the Direct Share row of the share sheet when the two match, so the literal has to be kept in step with that file.
*/
private const val SHARE_TARGET_CATEGORY = "io.element.android.category.SHARE_TARGET"
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultNotificationConversationService(
@@ -86,7 +92,8 @@ class DefaultNotificationConversationService(
}
val categories = setOfNotNull(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION else null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION else null,
SHARE_TARGET_CATEGORY,
)
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return
@@ -10,6 +10,7 @@ package io.element.android.libraries.push.impl.notifications.conversations
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.os.Build
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@@ -52,6 +53,27 @@ class DefaultNotificationConversationServiceTest : RobolectricTest() {
assertThat(shortcuts).isNotEmpty()
}
@Test
fun `the shortcut is offered to the share sheet as a conversation`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context
val service = createService(context)
service.onSendMessage(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
roomName = "Room title",
roomIsDirect = false,
roomAvatarUrl = null,
)
val shortcut = ShortcutManagerCompat.getDynamicShortcuts(context).single()
assertThat(shortcut.categories).containsAtLeast(
ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION,
// Matches the <share-target> category in app/src/main/res/xml/shortcuts.xml
"io.element.android.category.SHARE_TARGET",
)
}
@Test
fun `onLeftRoom removes a shortcut`() = runTest {
val context = InstrumentationRegistry.getInstrumentation().context