package invalid.lena.scrcpy; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.view.KeyEvent; import android.view.MotionEvent; import java.util.function.Consumer; // Encodes UI events into scrcpy ControlMessage byte arrays and pushes // them at ControlStream. Also mirrors the remote clipboard locally and // echoes local clipboard changes the other way. // // Forwards multi-pointer touch and AOSP keycodes. Mouse buttons and // scroll wheels are not forwarded yet. public final class Controller implements ControlStream.InboundSink { private final Consumer sender; private final ClipboardManager clipboard; public volatile String lastEvent = "(idle)"; // Held once so add/removePrimaryClipChangedListener see the same // listener reference. Method references generate fresh lambdas // each call site and the remove silently no-ops otherwise. private final ClipboardManager.OnPrimaryClipChangedListener clipListener = this::onLocalClipboardChanged; private volatile int targetW, targetH; private volatile int viewW, viewH; // Suppress one local clipboard change after we set it from a remote update. private volatile boolean suppressNextClipChange; public Controller(Context ctx, Consumer sender) { this.sender = sender; this.clipboard = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { clipboard.addPrimaryClipChangedListener(clipListener); } } public void release() { if (clipboard != null) { try { clipboard.removePrimaryClipChangedListener(clipListener); } catch (Exception ignored) {} } } public void setTargetSize(int w, int h) { targetW = w; targetH = h; Log.i("controller: target %dx%d", w, h); } public void setViewSize(int w, int h) { viewW = w; viewH = h; } // ---- inbound ---- @Override public void onRemoteClipboard(String text) { Log.i("clipboard from target: %d chars", text.length()); if (clipboard == null) return; suppressNextClipChange = true; try { clipboard.setPrimaryClip(ClipData.newPlainText("scrcpy-android", text)); } catch (Exception e) { Log.w("clipboard set local failed: %s", e); suppressNextClipChange = false; } } private void onLocalClipboardChanged() { if (suppressNextClipChange) { suppressNextClipChange = false; return; } if (clipboard == null) return; ClipData data; try { data = clipboard.getPrimaryClip(); } catch (Exception e) { Log.w("clipboard get local failed: %s", e); return; } if (data == null || data.getItemCount() == 0) return; CharSequence cs = data.getItemAt(0).coerceToText(null); if (cs == null) return; sendSetClipboard(cs.toString(), false); Log.i("clipboard to target: %d chars", cs.length()); } // ---- outbound ---- public void onTouch(MotionEvent ev) { int tw = targetW, th = targetH, vw = viewW, vh = viewH; if (tw == 0 || th == 0 || vw == 0 || vh == 0) return; // scrcpy's wire protocol uses ACTION_DOWN/UP/MOVE/CANCEL with a // pointerId per message. The server tracks which pointers are // currently down. So we translate Android's masked actions: // ACTION_POINTER_DOWN[i] -> ACTION_DOWN (this pointer joins) // ACTION_POINTER_UP[i] -> ACTION_UP (this pointer leaves) // ACTION_MOVE -> ACTION_MOVE for every current pointer // ACTION_CANCEL -> ACTION_CANCEL for every current pointer int action = ev.getActionMasked(); int idx = ev.getActionIndex(); int n = ev.getPointerCount(); switch (action) { case MotionEvent.ACTION_DOWN: sendPointer(ev, 0, MotionEvent.ACTION_DOWN, tw, th, vw, vh); break; case MotionEvent.ACTION_POINTER_DOWN: sendPointer(ev, idx, MotionEvent.ACTION_DOWN, tw, th, vw, vh); break; case MotionEvent.ACTION_UP: sendPointer(ev, 0, MotionEvent.ACTION_UP, tw, th, vw, vh); break; case MotionEvent.ACTION_POINTER_UP: sendPointer(ev, idx, MotionEvent.ACTION_UP, tw, th, vw, vh); break; case MotionEvent.ACTION_MOVE: for (int i = 0; i < n; i++) sendPointer(ev, i, MotionEvent.ACTION_MOVE, tw, th, vw, vh); break; case MotionEvent.ACTION_CANCEL: for (int i = 0; i < n; i++) sendPointer(ev, i, MotionEvent.ACTION_CANCEL, tw, th, vw, vh); break; default: return; } } // Sizes come from onTouch's snapshot of the volatile fields, so a // concurrent resize cannot zero a divisor between check and use. private void sendPointer(MotionEvent ev, int index, int action, int tw, int th, int vw, int vh) { long pointerId = ev.getPointerId(index); int x = (int) ev.getX(index); int y = (int) ev.getY(index); int tx = (int) ((long) x * tw / vw); int ty = (int) ((long) y * th / vh); // getPressure is calibrated around 1.0 but may exceed it on some // digitizers; clamp instead of masking so hard presses don't wrap // around to a light touch. int pressure = (action == MotionEvent.ACTION_UP) ? 0 : Math.min((int)(ev.getPressure(index) * 0xffff), 0xffff); sender.accept(ControlMessages.touch(action, pointerId, tx, ty, tw, th, pressure, /* actionButton */ 0, /* buttons */ 0)); lastEvent = "touch a=" + action + " (" + tx + "," + ty + ")"; } public void onKey(KeyEvent ev) { int tw = targetW, th = targetH; if (tw == 0 || th == 0) return; int action = ev.getAction(); // ACTION_DOWN=0, ACTION_UP=1 if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) return; sendKeycode(action, ev.getKeyCode(), ev.getRepeatCount(), ev.getMetaState()); } // Fire a tap of the target's Back. scrcpy server treats the // BACK_OR_SCREEN_ON message as Back when the target is on, screen- // on when it's off - useful for waking a locked target too. public void onBack() { sender.accept(ControlMessages.backOrScreenOn(ControlMessages.ACTION_DOWN)); sender.accept(ControlMessages.backOrScreenOn(ControlMessages.ACTION_UP)); lastEvent = "back"; } // ---- encoders (delegate to pure-java ControlMessages) ---- private void sendKeycode(int action, int keycode, int repeat, int metaState) { sender.accept(ControlMessages.keycode(action, keycode, repeat, metaState)); lastEvent = "key a=" + action + " kc=" + keycode; } private void sendSetClipboard(String text, boolean paste) { try { sender.accept(ControlMessages.setClipboard(/* sequence */ 0L, paste, text)); lastEvent = "clip " + text.length() + " chars"; } catch (IllegalArgumentException e) { Log.w("clipboard not sent: %s", e.getMessage()); } } }