Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions mac/Sources/DroidBridge/DeviceLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class DeviceLink {

enum Transport { case usb, wifi }

static let serverVersion = "0.2.0"
static let serverVersion = "0.2.1"
private static let remoteJar = "/data/local/tmp/droidbridge-server.jar"
private let log = Logger(subsystem: "dev.droidbridge", category: "link")

Expand All @@ -38,7 +38,12 @@ final class DeviceLink {
private var hardwareSerials: [String: String] = [:]

private(set) var state: State = .waiting {
didSet { if state != oldValue { let s = state; DispatchQueue.main.async { self.onState?(s) } } }
didSet {
guard state != oldValue else { return }
let s = state
log.notice("state: \(String(describing: s), privacy: .public)")
DispatchQueue.main.async { self.onState?(s) }
}
}

private let queue = DispatchQueue(label: "dev.droidbridge.link")
Expand Down Expand Up @@ -106,7 +111,7 @@ final class DeviceLink {
try connect(adb: adb, serial: serial)
readUntilClosed()
} catch {
log.error("connect failed: \(error.localizedDescription, privacy: .public)")
log.error("connect to \(serial, privacy: .public) failed: \(error.localizedDescription, privacy: .public)")
state = .failed(error.localizedDescription)
}
disconnect()
Expand Down Expand Up @@ -196,14 +201,34 @@ final class DeviceLink {

socket = try Self.connectLocal(port: p)
send(Wire.hello())
log.notice("connected to \(serial, privacy: .public)")
}

/// Seconds without anything from the device before the link counts as dead.
static let silenceLimit: TimeInterval = 8

private func readUntilClosed() {
var reader = FrameReader()
var buf = [UInt8](repeating: 0, count: 64 * 1024)
var lastHeard = Date()
var lastPing = Date()
while !stopped {
// The socket times out every second (SO_RCVTIMEO), so a device that stops answering
// without closing the connection (asleep, unplugged mid-write) is noticed.
if Date().timeIntervalSince(lastPing) >= 2 {
send(Wire.ping())
lastPing = Date()
}
let n = recv(socket, &buf, buf.count, 0)
if n < 0, errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR {
if Date().timeIntervalSince(lastHeard) > Self.silenceLimit {
log.notice("device silent for \(Int(Self.silenceLimit)) s, reconnecting")
break
}
continue
}
if n <= 0 { break }
lastHeard = Date()
do {
for message in try reader.feed(Array(buf[0..<n])) {
if case let .device(_, w, h, model) = message {
Expand All @@ -216,10 +241,12 @@ final class DeviceLink {
break
}
}
log.info("connection closed")
log.notice("connection closed")
}

private func disconnect() {
// Wakes a send blocked on a full socket before waiting for the write queue.
if socket >= 0 { shutdown(socket, SHUT_RDWR) }
writeQueue.sync {
if socket >= 0 { close(socket); socket = -1 }
}
Expand Down Expand Up @@ -281,6 +308,10 @@ final class DeviceLink {
var one: Int32 = 1
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, socklen_t(MemoryLayout<Int32>.size))
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, socklen_t(MemoryLayout<Int32>.size))
var second = timeval(tv_sec: 1, tv_usec: 0)
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &second, socklen_t(MemoryLayout<timeval>.size))
var sendLimit = timeval(tv_sec: 3, tv_usec: 0)
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &sendLimit, socklen_t(MemoryLayout<timeval>.size))
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = in_port_t(UInt16(port).bigEndian)
Expand Down
25 changes: 23 additions & 2 deletions mac/Sources/DroidBridge/InputCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ final class InputCapture {
private var tap: CFMachPort?
private var source: CFRunLoopSource?
private var crossed: Passage?
/// Where the cursor stays while the pointer is on the device.
private var frozenAt = CGPoint.zero
private var refreezes = 0
private var activePassages: [Passage] = []
/// After coming back, the cursor must move away from the edge before it can cross again.
private var armed = true
Expand Down Expand Up @@ -62,7 +65,9 @@ final class InputCapture {
source = CFMachPortCreateRunLoopSource(nil, tap, 0)
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)
log.info("event tap started")
log.notice("event tap started")
// Warping the cursor would otherwise mute the mouse for 0.25 s.
CGEventSource(stateID: .combinedSessionState)?.localEventsSuppressionInterval = 0
return true
}

Expand Down Expand Up @@ -130,17 +135,20 @@ final class InputCapture {
isRemote = true
crossed = passage
activePassages = all
frozenAt = CGEvent(source: nil)?.location ?? frozenAt
refreezes = 0
CGAssociateMouseAndMouseCursorPosition(0)
cursor.hide()
let returns = all.map { (side: $0.edge.androidSide, start: $0.deviceStart, end: $0.deviceEnd) }
send?(Wire.enter(side: passage.edge.androidSide, ratio: ratio, returns: returns))
log.info("to Android, ratio \(ratio)")
log.notice("to Android, ratio \(ratio)")
onRemoteChanged?(true)
}

private func forward(_ type: CGEventType, _ event: CGEvent) {
switch type {
case .mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged:
keepFrozen(event.location)
let dx = Int((event.getDoubleValueField(.mouseEventDeltaX) * options.speed).rounded())
let dy = Int((event.getDoubleValueField(.mouseEventDeltaY) * options.speed).rounded())
if dx != 0 || dy != 0 { send?(Wire.mouse(buttons: buttons, dx: dx, dy: dy)) }
Expand All @@ -163,6 +171,19 @@ final class InputCapture {
}
}

/// The cursor must stay put while the pointer is on the device. Detaching it from the mouse
/// (CGAssociateMouseAndMouseCursorPosition) does not always hold, e.g. right after the app starts,
/// so a cursor found away from its spot goes back and is detached again.
private func keepFrozen(_ location: CGPoint) {
guard abs(location.x - frozenAt.x) > 1 || abs(location.y - frozenAt.y) > 1 else { return }
CGWarpMouseCursorPosition(frozenAt)
CGAssociateMouseAndMouseCursorPosition(0)
refreezes += 1
if refreezes == 1 || refreezes % 100 == 0 {
log.notice("cursor was moving while on the device; frozen again (\(self.refreezes)x)")
}
}

private func scroll(_ event: CGEvent) {
let sign: Double = options.invertScroll ? -1 : 1
var wheel = 0
Expand Down
2 changes: 1 addition & 1 deletion mac/scripts/build-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Env: DROIDBRIDGE_VERSION (default 0.1.0), SIGN_IDENTITY (default "-", ad-hoc), SERVER_JAR (prebuilt jar).
set -euo pipefail
cd "$(dirname "$0")/.."
VERSION=${DROIDBRIDGE_VERSION:-0.2.0}
VERSION=${DROIDBRIDGE_VERSION:-0.2.1}
IDENTITY=${SIGN_IDENTITY:--}
APP=dist/DroidBridge.app

Expand Down
2 changes: 1 addition & 1 deletion server/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# build_without_gradle.sh. Needs ANDROID_HOME with platforms/android-36 and build-tools/36.0.0.
set -euo pipefail

VERSION=${DROIDBRIDGE_VERSION:-0.2.0}
VERSION=${DROIDBRIDGE_VERSION:-0.2.1}
PLATFORM=${ANDROID_PLATFORM:-36}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
: "${ANDROID_HOME:?set ANDROID_HOME}"
Expand Down
17 changes: 13 additions & 4 deletions server/src/dev/droidbridge/KeyboardLayouts.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ static boolean apply(String deviceName, String layout) {
return false;
}
try {
// The Mac sends its layout right after connecting; the keyboard may not be registered yet.
InputDevice device = null;
for (int id : InputDevice.getDeviceIds()) {
InputDevice d = InputDevice.getDevice(id);
if (d != null && deviceName.equals(d.getName())) {
device = d;
for (int attempt = 0; attempt < 30 && device == null; attempt++) {
for (int id : InputDevice.getDeviceIds()) {
InputDevice d = InputDevice.getDevice(id);
if (d != null && deviceName.equals(d.getName())) {
device = d;
}
}
if (device == null) {
Thread.sleep(100);
}
}
if (device == null) {
Expand All @@ -42,6 +48,9 @@ static boolean apply(String deviceName, String layout) {
set.invoke(im, identifier, PREFIX + layout);
Log.i("keyboard layout " + layout);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} catch (ReflectiveOperationException | RuntimeException e) {
Log.e("cannot set keyboard layout " + layout, e);
return false;
Expand Down
2 changes: 1 addition & 1 deletion tools/devtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

ADB = "adb"
REMOTE = "/data/local/tmp/droidbridge-server.jar"
VERSION = "0.2.0"
VERSION = "0.2.1"
LEFT, RIGHT, TOP, BOTTOM = 0, 1, 2, 3

def adb(*a):
Expand Down
Loading