2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00

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.
This commit is contained in:
Midori Kochiya 2024-06-11 14:01:03 +00:00 committed by Albert Vaca Cintora
parent f344586fb6
commit 96ecd620cf
10 changed files with 148 additions and 13 deletions

View File

@ -120,6 +120,9 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity> </activity>
<activity <activity
android:name="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" android:name="org.kde.kdeconnect.UserInterface.PluginSettingsActivity"

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<group android:pivotX="12" android:pivotY="12" android:scaleX="0.66" android:scaleY="0.66">
<path
android:fillColor="@android:color/white"
android:pathData="M21,2L3,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h7c1.1,0 2,-0.9 2,-2L23,4c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,4h18v12z" />
</group>
</vector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="@android:color/white"
android:pathData="M20,18c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2H4C2.9,4 2,4.9 2,6v10c0,1.1 0.9,2 2,2H0v2h24v-2H20zM4,6h16v10H4V6z" />
</group>
</vector>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="@android:color/white"
android:pathData="M16,1L8,1C6.34,1 5,2.34 5,4v16c0,1.66 1.34,3 3,3h8c1.66,0 3,-1.34 3,-3L19,4c0,-1.66 -1.34,-3 -3,-3zM14,21h-4v-1h4v1zM17.25,18L6.75,18L6.75,4h10.5v14z" />
</group>
</vector>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="@android:color/white"
android:pathData="M21,4L3,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h18c1.1,0 1.99,-0.9 1.99,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM19,18L5,18L5,6h14v12z" />
</group>
</vector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:pivotX="12"
android:pivotY="12"
android:scaleX="0.66"
android:scaleY="0.66">
<path
android:fillColor="@android:color/white"
android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12z" />
</group>
</vector>

7
res/xml/shortcuts.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<share-target android:targetClass="org.kde.kdeconnect.Plugins.SharePlugin.ShareActivity">
<data android:mimeType="*/*" />
<category android:name="org.kde.kdeconnect.category.SHARE_TARGET" />
</share-target>
</shortcuts>

View File

@ -22,13 +22,13 @@ import java.security.cert.CertificateException
* DeviceInfo contains all the properties needed to instantiate a Device. * DeviceInfo contains all the properties needed to instantiate a Device.
*/ */
class DeviceInfo( class DeviceInfo(
@JvmField val id : String, @JvmField val id: String,
@JvmField val certificate : Certificate, @JvmField val certificate: Certificate,
@JvmField var name : String, @JvmField var name: String,
@JvmField var type : DeviceType, @JvmField var type: DeviceType,
@JvmField var protocolVersion : Int = 0, @JvmField var protocolVersion: Int = 0,
@JvmField var incomingCapabilities : Set<String>? = null, @JvmField var incomingCapabilities: Set<String>? = null,
@JvmField var outgoingCapabilities : Set<String>? = null, @JvmField var outgoingCapabilities: Set<String>? = null,
) { ) {
/** /**
@ -40,7 +40,7 @@ class DeviceInfo(
try { try {
val encodedCertificate = Base64.encodeToString(certificate.encoded, 0) val encodedCertificate = Base64.encodeToString(certificate.encoded, 0)
with (settings.edit()) { with(settings.edit()) {
putString("certificate", encodedCertificate) putString("certificate", encodedCertificate)
putString("deviceName", name) putString("deviceName", name)
putString("deviceType", type.toString()) putString("deviceType", type.toString())
@ -73,7 +73,7 @@ class DeviceInfo(
*/ */
@JvmStatic @JvmStatic
@Throws(CertificateException::class) @Throws(CertificateException::class)
fun loadFromSettings(context : Context, deviceId: String, settings: SharedPreferences) = fun loadFromSettings(context: Context, deviceId: String, settings: SharedPreferences) =
with(settings) { with(settings) {
DeviceInfo( DeviceInfo(
id = deviceId, id = deviceId,
@ -104,8 +104,8 @@ class DeviceInfo(
@JvmStatic @JvmStatic
fun isValidIdentityPacket(identityPacket: NetworkPacket): Boolean = with(identityPacket) { fun isValidIdentityPacket(identityPacket: NetworkPacket): Boolean = with(identityPacket) {
type == NetworkPacket.PACKET_TYPE_IDENTITY && type == NetworkPacket.PACKET_TYPE_IDENTITY &&
DeviceHelper.filterName(getString("deviceName", "")).isNotBlank() && DeviceHelper.filterName(getString("deviceName", "")).isNotBlank() &&
getString("deviceId", "").isNotBlank() getString("deviceId", "").isNotBlank()
} }
} }
} }
@ -126,7 +126,7 @@ enum class DeviceType {
ContextCompat.getDrawable(context, toDrawableId())!! ContextCompat.getDrawable(context, toDrawableId())!!
@DrawableRes @DrawableRes
private fun toDrawableId() = fun toDrawableId() =
when (this) { when (this) {
PHONE -> R.drawable.ic_device_phone_32dp PHONE -> R.drawable.ic_device_phone_32dp
TABLET -> R.drawable.ic_device_tablet_32dp TABLET -> R.drawable.ic_device_tablet_32dp
@ -135,6 +135,15 @@ enum class DeviceType {
else -> R.drawable.ic_device_desktop_32dp 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 { companion object {
@JvmStatic @JvmStatic
fun fromString(s: String) = fun fromString(s: String) =

View File

@ -8,6 +8,7 @@ package org.kde.kdeconnect.Plugins.SharePlugin;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -160,7 +161,10 @@ public class ShareActivity extends AppCompatActivity {
super.onStart(); super.onStart();
final Intent intent = getIntent(); 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) { if (deviceId != null) {
SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SharePlugin.class); SharePlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId, SharePlugin.class);

View File

@ -24,6 +24,10 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat; 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 androidx.preference.PreferenceManager;
import org.apache.commons.lang3.ArrayUtils; 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.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment;
import org.kde.kdeconnect.async.BackgroundJob; import org.kde.kdeconnect.async.BackgroundJob;
import org.kde.kdeconnect.async.BackgroundJobHandler; import org.kde.kdeconnect.async.BackgroundJobHandler;
@ -43,6 +48,7 @@ import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -84,11 +90,34 @@ public class SharePlugin extends Plugin {
public boolean onCreate() { public boolean onCreate() {
super.onCreate(); super.onCreate();
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); 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 // Deliver URLs previously shared to this device now that it's connected
deliverPreviouslySentIntents(); deliverPreviouslySentIntents();
return true; return true;
} }
@Override
public void onDestroy() {
ShortcutManagerCompat.removeLongLivedShortcuts(context, List.of(device.getDeviceId()));
super.onDestroy();
}
private void deliverPreviouslySentIntents() { private void deliverPreviouslySentIntents() {
Set<String> currentUrlSet = mSharedPrefs.getStringSet(KEY_UNREACHABLE_URL_LIST + device.getDeviceId(), null); Set<String> currentUrlSet = mSharedPrefs.getStringSet(KEY_UNREACHABLE_URL_LIST + device.getDeviceId(), null);
if (currentUrlSet != null) { if (currentUrlSet != null) {