From 96ecd620cfec6717a11bde3e26605ca2f0e78911 Mon Sep 17 00:00:00 2001 From: Midori Kochiya Date: Tue, 11 Jun 2024 14:01:03 +0000 Subject: [PATCH] Add support for Direct Share targets As described in https://developer.android.com/training/sharing/direct-share-targets. This makes connected devices with `SharePlugin` enabled show up in Android's Sharesheet and can be directly shared to. --- AndroidManifest.xml | 3 ++ res/drawable/ic_device_desktop_shortcut.xml | 13 ++++++++ res/drawable/ic_device_laptop_shortcut.xml | 17 ++++++++++ res/drawable/ic_device_phone_shortcut.xml | 18 ++++++++++ res/drawable/ic_device_tablet_shortcut.xml | 18 ++++++++++ res/drawable/ic_device_tv_shortcut.xml | 17 ++++++++++ res/xml/shortcuts.xml | 7 ++++ src/org/kde/kdeconnect/DeviceInfo.kt | 33 ++++++++++++------- .../Plugins/SharePlugin/ShareActivity.java | 6 +++- .../Plugins/SharePlugin/SharePlugin.java | 29 ++++++++++++++++ 10 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 res/drawable/ic_device_desktop_shortcut.xml create mode 100644 res/drawable/ic_device_laptop_shortcut.xml create mode 100644 res/drawable/ic_device_phone_shortcut.xml create mode 100644 res/drawable/ic_device_tablet_shortcut.xml create mode 100644 res/drawable/ic_device_tv_shortcut.xml create mode 100644 res/xml/shortcuts.xml diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 02e1e178..2aa3db15 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -120,6 +120,9 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted + + + + + + + diff --git a/res/drawable/ic_device_laptop_shortcut.xml b/res/drawable/ic_device_laptop_shortcut.xml new file mode 100644 index 00000000..b1569ee5 --- /dev/null +++ b/res/drawable/ic_device_laptop_shortcut.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/res/drawable/ic_device_phone_shortcut.xml b/res/drawable/ic_device_phone_shortcut.xml new file mode 100644 index 00000000..f2b83b61 --- /dev/null +++ b/res/drawable/ic_device_phone_shortcut.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/res/drawable/ic_device_tablet_shortcut.xml b/res/drawable/ic_device_tablet_shortcut.xml new file mode 100644 index 00000000..fb214133 --- /dev/null +++ b/res/drawable/ic_device_tablet_shortcut.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/res/drawable/ic_device_tv_shortcut.xml b/res/drawable/ic_device_tv_shortcut.xml new file mode 100644 index 00000000..d2006224 --- /dev/null +++ b/res/drawable/ic_device_tv_shortcut.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/res/xml/shortcuts.xml b/res/xml/shortcuts.xml new file mode 100644 index 00000000..d8cca870 --- /dev/null +++ b/res/xml/shortcuts.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/org/kde/kdeconnect/DeviceInfo.kt b/src/org/kde/kdeconnect/DeviceInfo.kt index e02c243e..32fcb1f1 100644 --- a/src/org/kde/kdeconnect/DeviceInfo.kt +++ b/src/org/kde/kdeconnect/DeviceInfo.kt @@ -22,13 +22,13 @@ import java.security.cert.CertificateException * DeviceInfo contains all the properties needed to instantiate a Device. */ class DeviceInfo( - @JvmField val id : String, - @JvmField val certificate : Certificate, - @JvmField var name : String, - @JvmField var type : DeviceType, - @JvmField var protocolVersion : Int = 0, - @JvmField var incomingCapabilities : Set? = null, - @JvmField var outgoingCapabilities : Set? = null, + @JvmField val id: String, + @JvmField val certificate: Certificate, + @JvmField var name: String, + @JvmField var type: DeviceType, + @JvmField var protocolVersion: Int = 0, + @JvmField var incomingCapabilities: Set? = null, + @JvmField var outgoingCapabilities: Set? = null, ) { /** @@ -40,7 +40,7 @@ class DeviceInfo( try { val encodedCertificate = Base64.encodeToString(certificate.encoded, 0) - with (settings.edit()) { + with(settings.edit()) { putString("certificate", encodedCertificate) putString("deviceName", name) putString("deviceType", type.toString()) @@ -73,7 +73,7 @@ class DeviceInfo( */ @JvmStatic @Throws(CertificateException::class) - fun loadFromSettings(context : Context, deviceId: String, settings: SharedPreferences) = + fun loadFromSettings(context: Context, deviceId: String, settings: SharedPreferences) = with(settings) { DeviceInfo( id = deviceId, @@ -104,8 +104,8 @@ class DeviceInfo( @JvmStatic fun isValidIdentityPacket(identityPacket: NetworkPacket): Boolean = with(identityPacket) { type == NetworkPacket.PACKET_TYPE_IDENTITY && - DeviceHelper.filterName(getString("deviceName", "")).isNotBlank() && - getString("deviceId", "").isNotBlank() + DeviceHelper.filterName(getString("deviceName", "")).isNotBlank() && + getString("deviceId", "").isNotBlank() } } } @@ -126,7 +126,7 @@ enum class DeviceType { ContextCompat.getDrawable(context, toDrawableId())!! @DrawableRes - private fun toDrawableId() = + fun toDrawableId() = when (this) { PHONE -> R.drawable.ic_device_phone_32dp TABLET -> R.drawable.ic_device_tablet_32dp @@ -135,6 +135,15 @@ enum class DeviceType { else -> R.drawable.ic_device_desktop_32dp } + fun toShortcutDrawableId() = + when (this) { + PHONE -> R.drawable.ic_device_phone_shortcut + TABLET -> R.drawable.ic_device_tablet_shortcut + TV -> R.drawable.ic_device_tv_shortcut + LAPTOP -> R.drawable.ic_device_laptop_shortcut + else -> R.drawable.ic_device_desktop_shortcut + } + companion object { @JvmStatic fun fromString(s: String) = diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java index f8dc761f..188fa354 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareActivity.java @@ -8,6 +8,7 @@ package org.kde.kdeconnect.Plugins.SharePlugin; import android.content.Intent; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; @@ -160,7 +161,10 @@ public class ShareActivity extends AppCompatActivity { super.onStart(); final Intent intent = getIntent(); - final String deviceId = intent.getStringExtra("deviceId"); + String deviceId = intent.getStringExtra("deviceId"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && deviceId == null) { + deviceId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID); + } if (deviceId != null) { SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SharePlugin.class); diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java index 2f9c7aa5..a064979e 100644 --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java @@ -24,6 +24,10 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.core.content.ContextCompat; +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.preference.PreferenceManager; import org.apache.commons.lang3.ArrayUtils; @@ -33,6 +37,7 @@ import org.kde.kdeconnect.Helpers.IntentHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; +import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect.async.BackgroundJob; import org.kde.kdeconnect.async.BackgroundJobHandler; @@ -43,6 +48,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; /** @@ -84,11 +90,34 @@ public class SharePlugin extends Plugin { public boolean onCreate() { super.onCreate(); mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + Intent shortcutIntent = new Intent(context, MainActivity.class); + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.putExtra(MainActivity.EXTRA_DEVICE_ID, device.getDeviceId()); + + IconCompat icon = IconCompat.createWithResource(context, device.getDeviceType().toShortcutDrawableId()); + + ShortcutInfoCompat shortcut = new ShortcutInfoCompat + .Builder(context, device.getDeviceId()) + .setIntent(shortcutIntent) + .setIcon(icon) + .setShortLabel(device.getName()) + .setCategories(Set.of("org.kde.kdeconnect.category.SHARE_TARGET")) + .setLocusId(new LocusIdCompat(device.getDeviceId())) + .build(); + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut); + // Deliver URLs previously shared to this device now that it's connected deliverPreviouslySentIntents(); return true; } + @Override + public void onDestroy() { + ShortcutManagerCompat.removeLongLivedShortcuts(context, List.of(device.getDeviceId())); + super.onDestroy(); + } + private void deliverPreviouslySentIntents() { Set currentUrlSet = mSharedPrefs.getStringSet(KEY_UNREACHABLE_URL_LIST + device.getDeviceId(), null); if (currentUrlSet != null) {