mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-22 01:51:47 +00:00
Remove DevicePacketQueue, use a IO coroutine to send packets
This commit is contained in:
parent
66ea01ad29
commit
4ae6e50020
@ -18,6 +18,12 @@ import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.collections4.MultiValuedMap
|
||||
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap
|
||||
import org.kde.kdeconnect.Backends.BaseLink
|
||||
@ -56,7 +62,6 @@ class Device : PacketReceiver {
|
||||
var pairingHandler: PairingHandler
|
||||
private val pairingCallbacks = CopyOnWriteArrayList<PairingCallback>()
|
||||
private val links = CopyOnWriteArrayList<BaseLink>()
|
||||
private var packetQueue: DevicePacketQueue? = null
|
||||
var supportedPlugins: List<String>
|
||||
private set
|
||||
val loadedPlugins: ConcurrentMap<String, Plugin> = ConcurrentHashMap()
|
||||
@ -65,6 +70,9 @@ class Device : PacketReceiver {
|
||||
private var pluginsByIncomingInterface: MultiValuedMap<String, String> = ArrayListValuedHashMap()
|
||||
private val settings: SharedPreferences
|
||||
private val pluginsChangedListeners: MutableList<PluginsChangedListener> = CopyOnWriteArrayList()
|
||||
class NetworkPacketWithCallback(val np : NetworkPacket, val callback: SendPacketStatusCallback)
|
||||
private val sendChannel = Channel<NetworkPacketWithCallback>(Channel.UNLIMITED)
|
||||
private var sendCoroutine : Job? = null
|
||||
|
||||
/**
|
||||
* Constructor for remembered, already-trusted devices.
|
||||
@ -275,10 +283,9 @@ class Device : PacketReceiver {
|
||||
get() = links.isNotEmpty()
|
||||
|
||||
fun addLink(link: BaseLink) {
|
||||
if (links.isEmpty()) {
|
||||
packetQueue = DevicePacketQueue(this)
|
||||
if (sendCoroutine == null) {
|
||||
launchSendCoroutine()
|
||||
}
|
||||
|
||||
// FilesHelper.LogOpenFileCount();
|
||||
links.add(link)
|
||||
|
||||
@ -306,9 +313,15 @@ class Device : PacketReceiver {
|
||||
)
|
||||
if (links.isEmpty()) {
|
||||
reloadPluginsFromSettings()
|
||||
if (packetQueue != null) {
|
||||
packetQueue!!.disconnected()
|
||||
packetQueue = null
|
||||
sendCoroutine?.cancel(CancellationException("Device disconnected"))
|
||||
sendCoroutine = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSendCoroutine() {
|
||||
sendCoroutine = CoroutineScope(Dispatchers.IO).launch {
|
||||
for (item in sendChannel) {
|
||||
sendPacketBlocking(item.np, item.callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -410,49 +423,26 @@ class Device : PacketReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun sendPacket(np: NetworkPacket) = sendPacket(np, -1, defaultCallback)
|
||||
|
||||
@AnyThread
|
||||
fun sendPacket(np: NetworkPacket, replaceID: Int) = sendPacket(np, replaceID, defaultCallback)
|
||||
|
||||
@WorkerThread
|
||||
fun sendPacketBlocking(np: NetworkPacket): Boolean = sendPacketBlocking(np, defaultCallback)
|
||||
|
||||
@AnyThread
|
||||
fun sendPacket(np: NetworkPacket, callback: SendPacketStatusCallback) = sendPacket(np, -1, callback)
|
||||
|
||||
/**
|
||||
* Send a packet to the device asynchronously
|
||||
* @param np The packet
|
||||
* @param replaceID If positive, replaces all unsent packets with the same replaceID
|
||||
* @param callback A callback for success/failure
|
||||
*/
|
||||
@AnyThread
|
||||
fun sendPacket(np: NetworkPacket, replaceID: Int, callback: SendPacketStatusCallback) {
|
||||
val packetQueue = packetQueue ?: run {
|
||||
callback.onFailure(Exception("Device disconnected!"))
|
||||
return
|
||||
}
|
||||
// TODO: Migrate to coroutine version (addPacket)
|
||||
packetQueue.addPacket(np, replaceID, callback)
|
||||
fun sendPacket(np: NetworkPacket, callback: SendPacketStatusCallback) {
|
||||
sendChannel.trySend(NetworkPacketWithCallback(np, callback))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we still have an unsent packet in the queue with the given ID.
|
||||
* If so, remove it from the queue and return it
|
||||
* @param replaceID The replace ID (must be positive)
|
||||
* @return The found packet, or null
|
||||
*/
|
||||
fun getAndRemoveUnsentPacket(replaceID: Int): NetworkPacket? {
|
||||
// TODO: Migrate to coroutine version (getAndRemoveUnsentPacket)
|
||||
return packetQueue?.getAndRemoveUnsentPacket(replaceID)
|
||||
}
|
||||
@AnyThread
|
||||
fun sendPacket(np: NetworkPacket) = sendPacket(np, defaultCallback)
|
||||
|
||||
@WorkerThread
|
||||
fun sendPacketBlocking(np: NetworkPacket, callback: SendPacketStatusCallback): Boolean =
|
||||
sendPacketBlocking(np, callback, false)
|
||||
|
||||
@WorkerThread
|
||||
fun sendPacketBlocking(np: NetworkPacket): Boolean = sendPacketBlocking(np, defaultCallback, false)
|
||||
|
||||
/**
|
||||
* Send `np` over one of this device's connected [.links].
|
||||
*
|
||||
|
@ -1,142 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019 Matthijs Tijink <matthijstijink@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Keeps a queue of packets to send to a device, to prevent either blocking or using lots of threads
|
||||
*/
|
||||
class DevicePacketQueue {
|
||||
/**
|
||||
* Holds the packet and related stuff to keep in the queue
|
||||
*/
|
||||
private static final class Item {
|
||||
NetworkPacket packet;
|
||||
/**
|
||||
* Replacement ID: if positive, it can be replaced by later packets with the same ID
|
||||
*/
|
||||
final int replaceID;
|
||||
Device.SendPacketStatusCallback callback;
|
||||
|
||||
Item(NetworkPacket packet, int replaceID, Device.SendPacketStatusCallback callback) {
|
||||
this.packet = packet;
|
||||
this.callback = callback;
|
||||
this.replaceID = replaceID;
|
||||
}
|
||||
}
|
||||
|
||||
private final ArrayDeque<Item> items = new ArrayDeque<>();
|
||||
private final Device mDevice;
|
||||
private final Object lock = new Object();
|
||||
private boolean exit = false;
|
||||
|
||||
DevicePacketQueue(Device device) {
|
||||
this(device, true);
|
||||
}
|
||||
|
||||
DevicePacketQueue(Device device, Boolean startThread) {
|
||||
mDevice = device;
|
||||
if (startThread) {
|
||||
ThreadHelper.execute(new SendingRunnable());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet (at some point in the future)
|
||||
* @param packet The packet
|
||||
* @param replaceID If positive, it will replace all older packets still in the queue
|
||||
* @param callback The callback after sending the packet
|
||||
*/
|
||||
void addPacket(NetworkPacket packet, int replaceID, Device.SendPacketStatusCallback callback) {
|
||||
synchronized (lock) {
|
||||
if (exit) {
|
||||
callback.onFailure(new Exception("Device disconnected!"));
|
||||
} else {
|
||||
boolean replaced = false;
|
||||
|
||||
if (replaceID >= 0) {
|
||||
for (Item item : items) {
|
||||
if (item.replaceID == replaceID) {
|
||||
item.packet = packet;
|
||||
item.callback = callback;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced) {
|
||||
items.addLast(new Item(packet, replaceID, callback));
|
||||
lock.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we still have an unsent packet in the queue with the given ID.
|
||||
* If so, remove it from the queue and return it
|
||||
* @param replaceID The replace ID (must be positive)
|
||||
* @return The found packet, or null
|
||||
*/
|
||||
NetworkPacket getAndRemoveUnsentPacket(int replaceID) {
|
||||
synchronized (lock) {
|
||||
final Optional<Item> itemOptional = items.stream()
|
||||
.filter(item -> item.replaceID == replaceID).findFirst();
|
||||
if (itemOptional.isPresent()) {
|
||||
final Item item = itemOptional.get();
|
||||
items.remove(item);
|
||||
return item.packet;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void disconnected() {
|
||||
synchronized (lock) {
|
||||
exit = true;
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private final class SendingRunnable implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
Item item;
|
||||
synchronized (lock) {
|
||||
while (items.isEmpty() && !exit) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
if (exit) {
|
||||
Log.i("DevicePacketQueue", "Terminating sending loop");
|
||||
break;
|
||||
}
|
||||
|
||||
item = items.removeFirst();
|
||||
}
|
||||
|
||||
mDevice.sendPacketBlocking(item.packet, item.callback);
|
||||
}
|
||||
|
||||
while (!items.isEmpty()) {
|
||||
Item item = items.removeFirst();
|
||||
item.callback.onFailure(new Exception("Device disconnected!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -281,9 +281,6 @@ class NetworkPacket private constructor(
|
||||
const val PACKET_TYPE_IDENTITY: String = "kdeconnect.identity"
|
||||
const val PACKET_TYPE_PAIR: String = "kdeconnect.pair"
|
||||
|
||||
const val PACKET_REPLACEID_MOUSEMOVE: Int = 0
|
||||
const val PACKET_REPLACEID_PRESENTERPOINTER: Int = 1
|
||||
|
||||
@JvmStatic
|
||||
@Throws(JSONException::class)
|
||||
fun unserialize(s: String): NetworkPacket {
|
||||
|
@ -91,19 +91,10 @@ public class MousePadPlugin extends Plugin {
|
||||
}
|
||||
|
||||
public void sendMouseDelta(float dx, float dy) {
|
||||
NetworkPacket np = getDevice().getAndRemoveUnsentPacket(NetworkPacket.PACKET_REPLACEID_MOUSEMOVE);
|
||||
if (np == null) {
|
||||
np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
} else {
|
||||
// TODO: In my tests we never get here. Decide if it's worth keeping the logic to replace unsent packets.
|
||||
dx += np.getInt("dx");
|
||||
dy += np.getInt("dx");
|
||||
}
|
||||
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("dx", dx);
|
||||
np.set("dy", dy);
|
||||
|
||||
getDevice().sendPacket(np, NetworkPacket.PACKET_REPLACEID_MOUSEMOVE);
|
||||
getDevice().sendPacket(np);
|
||||
}
|
||||
|
||||
public void sendLeftClick() {
|
||||
|
@ -111,21 +111,13 @@ public class PresenterPlugin extends Plugin {
|
||||
}
|
||||
|
||||
public void sendPointer(float xDelta, float yDelta) {
|
||||
NetworkPacket np = getDevice().getAndRemoveUnsentPacket(NetworkPacket.PACKET_REPLACEID_PRESENTERPOINTER);
|
||||
if (np == null) {
|
||||
np = new NetworkPacket(PACKET_TYPE_PRESENTER);
|
||||
} else {
|
||||
// TODO: In my tests we never get here. Decide if it's worth keeping the logic to replace unsent packets.
|
||||
xDelta += np.getInt("dx");
|
||||
yDelta += np.getInt("dy");
|
||||
}
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_PRESENTER);
|
||||
np.set("dx", xDelta);
|
||||
np.set("dy", yDelta);
|
||||
getDevice().sendPacket(np, NetworkPacket.PACKET_REPLACEID_PRESENTERPOINTER);
|
||||
getDevice().sendPacket(np);
|
||||
}
|
||||
|
||||
public void stopPointer() {
|
||||
getDevice().getAndRemoveUnsentPacket(NetworkPacket.PACKET_REPLACEID_PRESENTERPOINTER);
|
||||
NetworkPacket np = new NetworkPacket(PACKET_TYPE_PRESENTER);
|
||||
np.set("stop", true);
|
||||
getDevice().sendPacket(np);
|
||||
|
@ -1,50 +0,0 @@
|
||||
package org.kde.kdeconnect;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class DevicePacketQueueTest {
|
||||
@Test
|
||||
public void addPacketWithPositiveReplaceId() {
|
||||
Device device = Mockito.mock(Device.class);
|
||||
Device.SendPacketStatusCallback callback = Mockito.mock(Device.SendPacketStatusCallback.class);
|
||||
|
||||
DevicePacketQueue queue = new DevicePacketQueue(device, false);
|
||||
|
||||
queue.addPacket(new NetworkPacket("Test"), 0, callback);
|
||||
queue.addPacket(new NetworkPacket("Test1"), 1, callback);
|
||||
|
||||
assertNotNull(queue.getAndRemoveUnsentPacket(0));
|
||||
assertNotNull(queue.getAndRemoveUnsentPacket(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPacketWithNegativeReplaceId() {
|
||||
Device device = Mockito.mock(Device.class);
|
||||
Device.SendPacketStatusCallback callback = Mockito.mock(Device.SendPacketStatusCallback.class);
|
||||
|
||||
DevicePacketQueue queue = new DevicePacketQueue(device, false);
|
||||
|
||||
queue.addPacket(new NetworkPacket("Test"), -1, callback);
|
||||
queue.addPacket(new NetworkPacket("Test1"), -1, callback);
|
||||
|
||||
assertNotNull(queue.getAndRemoveUnsentPacket(-1));
|
||||
assertNotNull(queue.getAndRemoveUnsentPacket(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addPacketReplacesPacket() {
|
||||
Device device = Mockito.mock(Device.class);
|
||||
Device.SendPacketStatusCallback callback = Mockito.mock(Device.SendPacketStatusCallback.class);
|
||||
|
||||
DevicePacketQueue queue = new DevicePacketQueue(device, false);
|
||||
|
||||
queue.addPacket(new NetworkPacket("Test"), 1, callback);
|
||||
queue.addPacket(new NetworkPacket("Test1"), 1, callback);
|
||||
|
||||
assertNotNull(queue.getAndRemoveUnsentPacket(1));
|
||||
assertNull(queue.getAndRemoveUnsentPacket(1));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user