Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ public void notifyCrash(Exception e) {
new ComputerDetails.AddressTuple(host, port),
httpsPort, uniqueId, config,
PlatformBinding.getCryptoProvider(this), serverCert);

// Give StreamView a reference to conn so its InputConnection
// can route IME commitText() (voice dictation, swipe typing) to the host
streamView.setNvConnection(conn);

controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
keyboardTranslator = new KeyboardTranslator();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.limelight.binding.input;

import android.view.View;
import android.view.inputmethod.BaseInputConnection;

import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.KeyboardPacket;

/**
* Custom InputConnection that intercepts commitText() from the Android IME
* (voice dictation, autocomplete, swipe typing) and routes the committed text
* to the remote host via the Moonlight streaming protocol.
*
* This is an additive fix — individual KeyEvent handling via onKeyDown/onKeyUp
* continues to work unchanged through StreamView.onKeyPreIme().
*
* Copyright (C) 2024 Jon Gutierrez / MN Compute
* License: GPL-3.0-or-later (same as upstream moonlight-android)
*/
public class StreamInputConnection extends BaseInputConnection {
private final NvConnection conn;

// VK key codes in Moonlight's wire format: (KEY_PREFIX << 8) | windowsVkCode
// KEY_PREFIX = 0x80 (see KeyboardTranslator.KEY_PREFIX)
// VK_BACK = 0x08 (backspace), VK_DELETE = 0x2E (forward delete)
private static final short VK_BACKSPACE = (short) ((0x80 << 8) | 0x08);
private static final short VK_DELETE = (short) ((0x80 << 8) | 0x2E);

public StreamInputConnection(View targetView, boolean fullEditor, NvConnection conn) {
super(targetView, fullEditor);
this.conn = conn;
}

/**
* Called when the IME finalizes text input — voice dictation result,
* autocomplete selection, swipe-typed word, etc.
* Route the full text string to the remote host.
*/
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (text != null && text.length() > 0 && conn != null) {
// sendUtf8Text accepts full strings — no need for char-by-char loop
conn.sendUtf8Text(text.toString());
}
return true;
}

/**
* Called with in-progress (composing) text while the user is still
* dictating or typing. Don't send this — only send on commitText()
* when the text is finalized.
*/
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
// Intentionally do nothing with composing text
return true;
}

/**
* Called when the IME finishes composition. Nothing to do here since
* we handle everything in commitText().
*/
@Override
public boolean finishComposingText() {
return true;
}

/**
* Called by the IME to delete surrounding text (e.g., voice correction
* "delete that", or backspace from swipe keyboard).
* Send the appropriate number of backspace or delete key events.
*/
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
if (conn != null) {
for (int i = 0; i < beforeLength; i++) {
conn.sendKeyboardInput(VK_BACKSPACE, KeyboardPacket.KEY_DOWN, (byte) 0, (byte) 0);
conn.sendKeyboardInput(VK_BACKSPACE, KeyboardPacket.KEY_UP, (byte) 0, (byte) 0);
}
for (int i = 0; i < afterLength; i++) {
conn.sendKeyboardInput(VK_DELETE, KeyboardPacket.KEY_DOWN, (byte) 0, (byte) 0);
conn.sendKeyboardInput(VK_DELETE, KeyboardPacket.KEY_UP, (byte) 0, (byte) 0);
}
}
return true;
}
}
25 changes: 25 additions & 0 deletions app/src/main/java/com/limelight/ui/StreamView.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@

import android.annotation.TargetApi;
import android.content.Context;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.SurfaceView;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;

import com.limelight.binding.input.StreamInputConnection;
import com.limelight.nvstream.NvConnection;

public class StreamView extends SurfaceView {
private double desiredAspectRatio;
private InputCallbacks inputCallbacks;

// Connection reference for IME text commit routing
private NvConnection nvConn;

public void setNvConnection(NvConnection conn) {
this.nvConn = conn;
}

public void setDesiredAspectRatio(double aspectRatio) {
this.desiredAspectRatio = aspectRatio;
}
Expand Down Expand Up @@ -78,6 +91,18 @@ else if (event.getAction() == KeyEvent.ACTION_UP) {
return super.onKeyPreIme(keyCode, event);
}

/**
* Provide a custom InputConnection so that IME commitText() events
* (voice dictation, swipe typing, autocomplete) are routed to the remote host.
* Individual key events continue to flow through onKeyPreIme() as before.
*/
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_NONE;
return new StreamInputConnection(this, false, nvConn);
}

public interface InputCallbacks {
boolean handleKeyUp(KeyEvent event);
boolean handleKeyDown(KeyEvent event);
Expand Down