diff --git a/app/build.gradle b/app/build.gradle index 413f3ba75..ae9e4ef69 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,15 +136,14 @@ android { } dependencies { - implementation 'org.bouncycastle:bcprov-jdk15on:1.70' - implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' - implementation 'org.jcodec:jcodec:0.2.3' - implementation 'com.squareup.okhttp3:okhttp:3.12.13' - implementation 'com.squareup.okio:okio:1.17.5' - // 3.5.8 requires minSdk 19, uses StandardCharsets.UTF_8 internally - implementation 'org.jmdns:jmdns:3.5.7' + implementation 'org.bouncycastle:bcprov-jdk18on:1.77' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' + implementation 'org.jcodec:jcodec:0.2.5' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'org.jmdns:jmdns:3.5.9' implementation 'com.github.cgutman:ShieldControllerExtensions:1.0.1' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.github.ZeyuKeithFu:KeyboardHeaderLayout:v1.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0-alpha09' + implementation 'com.google.code.gson:gson:2.11.0' } diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index b876183fb..22aef78e7 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -82,6 +82,8 @@ import android.widget.TextView; import android.widget.Toast; +import org.json.JSONException; + import java.io.ByteArrayInputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -92,8 +94,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Locale; -import java.util.Optional; +import java.util.Objects; public class Game extends Activity implements SurfaceHolder.Callback, @@ -192,6 +196,7 @@ public void onServiceDisconnected(ComponentName componentName) { public static final String EXTRA_PC_NAME = "PcName"; public static final String EXTRA_APP_HDR = "HDR"; public static final String EXTRA_SERVER_CERT = "ServerCert"; + public static final String EXTRA_APP_CMD = "CmdList"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -334,8 +339,12 @@ public boolean onCapturedPointer(View view, MotionEvent motionEvent) { String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); boolean appSupportsHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false); byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT); + String cmdList = Game.this.getIntent().getStringExtra(EXTRA_APP_CMD); app = new NvApp(appName != null ? appName : "app", appId, appSupportsHdr); + if (cmdList != null) { + app.setCmdList(cmdList); + } X509Certificate serverCert = null; try { if (derCertData != null) { @@ -2810,7 +2819,7 @@ public void onUsbPermissionPromptCompleted() { @Override public void showGameMenu(GameInputDevice device) { - new GameMenu(this, conn, device); + new GameMenu(this, app, conn, device); } @Override @@ -2852,21 +2861,6 @@ public void togglePerformanceOverlay() { performanceOverlayView.setVisibility(requestedPerformanceOverlayVisibility); } - public void imeSwitch() { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - List mInputMethodProperties = imm.getInputMethodList(); - Optional hackersInput = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - hackersInput = mInputMethodProperties.stream().filter(m -> m.getId().startsWith("org.pocketworkstation.pckeyboard")).findFirst(); - } - imm.showInputMethodPicker(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (!hackersInput.isPresent()) { - Toast.makeText(Game.this, "杂鱼~❤ 还不快装黑客键盘(hacker's keyboard", Toast.LENGTH_LONG).show(); - } - } - } - private static byte getModifier(short key) { switch (key) { case KeyboardTranslator.VK_LSHIFT: diff --git a/app/src/main/java/com/limelight/GameMenu.java b/app/src/main/java/com/limelight/GameMenu.java index 105b1cff0..415558807 100644 --- a/app/src/main/java/com/limelight/GameMenu.java +++ b/app/src/main/java/com/limelight/GameMenu.java @@ -4,11 +4,21 @@ import android.os.Handler; import android.widget.ArrayAdapter; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.limelight.binding.input.GameInputDevice; import com.limelight.binding.input.KeyboardTranslator; import com.limelight.nvstream.NvConnection; +import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.input.KeyboardPacket; +import com.limelight.utils.ServerHelper; +import org.json.JSONArray; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -39,11 +49,13 @@ public MenuOption(String label, Runnable runnable) { } private final Game game; + private final NvApp app; private final NvConnection conn; private final GameInputDevice device; - public GameMenu(Game game, NvConnection conn, GameInputDevice device) { + public GameMenu(Game game, NvApp app, NvConnection conn, GameInputDevice device) { this.game = game; + this.app = app; this.conn = conn; this.device = device; @@ -178,10 +190,25 @@ private void showMenu() { options.addAll(device.getGameMenuOptions()); } + JsonArray cmdList = app.getCmdList(); + if (cmdList != null) { + for (int i = 0; i < ((JsonArray) cmdList).size(); i++) { + JsonObject cmd = cmdList.get(i).getAsJsonObject(); + options.add(new MenuOption(cmd.get("name").getAsString(), () -> + { + try { + conn.sendSuperCmd(cmd.get("id").getAsString()); + } catch (IOException | XmlPullParserException e) { + throw new RuntimeException(e); + } + } + )); + } + } + options.add(new MenuOption(getString(R.string.game_menu_toggle_performance_overlay), () -> game.togglePerformanceOverlay())); options.add(new MenuOption(getString(R.string.game_menu_send_keys), () -> showSpecialKeysMenu())); - options.add(new MenuOption(getString(R.string.game_menu_switch_ime), () -> game.imeSwitch())); - options.add(new MenuOption(getString(R.string.game_menu_disconnect), () -> game.disconnect())); + options.add(new MenuOption(getString(R.string.game_menu_disconnect), true, () -> game.disconnect())); options.add(new MenuOption(getString(R.string.game_menu_cancel), null)); showMenuDialog("Game Menu", options.toArray(new MenuOption[options.size()])); diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index ae116a6de..e3296f149 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -19,8 +19,6 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; -import java.util.Timer; -import java.util.TimerTask; import java.util.concurrent.Semaphore; import javax.crypto.KeyGenerator; @@ -588,4 +586,23 @@ public void sendUtf8Text(final String text) { public static String findExternalAddressForMdns(String stunHostname, int stunPort) { return MoonBridge.findExternalAddressIP4(stunHostname, stunPort); } + + public void sendSuperCmd(String cmdId) throws IOException, XmlPullParserException { + new Thread(new Runnable() { + @Override + public void run() { + NvHTTP h = null; + try { + h = new NvHTTP(context.serverAddress, context.httpsPort, uniqueId, context.serverCert, cryptoProvider); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + h.sendSuperCmd(cmdId); + } catch (IOException | XmlPullParserException e) { + throw new RuntimeException(e); + } + } + }).start(); + } } diff --git a/app/src/main/java/com/limelight/nvstream/http/NvApp.java b/app/src/main/java/com/limelight/nvstream/http/NvApp.java index bb5a1072c..d5f6fe96a 100644 --- a/app/src/main/java/com/limelight/nvstream/http/NvApp.java +++ b/app/src/main/java/com/limelight/nvstream/http/NvApp.java @@ -1,12 +1,18 @@ package com.limelight.nvstream.http; +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.limelight.LimeLog; + public class NvApp { private String appName = ""; private int appId; private boolean initialized; private boolean hdrSupported; + private JsonArray cmdList; public NvApp() {} @@ -59,12 +65,22 @@ public boolean isInitialized() { return this.initialized; } + public void setCmdList(String cmdList) { + this.cmdList = new Gson().fromJson(cmdList, JsonArray.class); + } + + public JsonArray getCmdList() { + return this.cmdList; + } + + @NonNull @Override public String toString() { StringBuilder str = new StringBuilder(); str.append("Name: ").append(appName).append("\n"); str.append("HDR Supported: ").append(hdrSupported ? "Yes" : "Unknown").append("\n"); str.append("ID: ").append(appId).append("\n"); + if (cmdList!= null) str.append("Super CMDs: ").append(cmdList.toString()).append("\n"); return str.toString(); } } diff --git a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java index 4b17bc585..414f8dbf2 100644 --- a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java +++ b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java @@ -24,6 +24,7 @@ import java.security.cert.X509Certificate; import java.util.LinkedList; import java.util.ListIterator; +import java.util.Objects; import java.util.Stack; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -403,16 +404,7 @@ private OkHttpClient performAndroidTlsHack(OkHttpClient client) { try { SSLContext sc = SSLContext.getInstance("TLS"); sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom()); - - // TLS 1.2 is not enabled by default prior to Android 5.0, so we'll need a custom - // SSLSocketFactory in order to connect to GFE 3.20.4 which requires TLSv1.2 or later. - // We don't just always use TLSv12SocketFactory because explicitly specifying TLS versions - // prevents later TLS versions from being negotiated even if client and server otherwise - // support them. - return client.newBuilder().sslSocketFactory( - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? - sc.getSocketFactory() : new TLSv12SocketFactory(sc), - trustManager).build(); + return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build(); } catch (NoSuchAlgorithmException | KeyManagementException e) { throw new RuntimeException(e); } @@ -659,6 +651,12 @@ public static LinkedList getAppListByReader(Reader r) throws XmlPullParse app.setAppId(xpp.getText()); } else if (currentTag.peek().equals("IsHdrSupported")) { app.setHdrSupported(xpp.getText().equals("1")); + } else if (currentTag.peek().equals("SuperCmds")) { + String cmdListStr = xpp.getText(); + LimeLog.info(cmdListStr + " appcmds"); + if (!Objects.equals(cmdListStr, "null")) { + app.setCmdList(xpp.getText()); + } } break; } @@ -821,61 +819,8 @@ public boolean pcSleep() throws IOException, XmlPullParserException { return !getXmlString(xmlStr, "pcsleep", true).equals("0"); } - // Based on example code from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/ - private static class TLSv12SocketFactory extends SSLSocketFactory { - private SSLSocketFactory internalSSLSocketFactory; - - public TLSv12SocketFactory(SSLContext context) { - internalSSLSocketFactory = context.getSocketFactory(); - } - - @Override - public String[] getDefaultCipherSuites() { - return internalSSLSocketFactory.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return internalSSLSocketFactory.getSupportedCipherSuites(); - } - - @Override - public Socket createSocket() throws IOException { - return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket()); - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { - return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket(String host, int port) throws IOException { - return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { - return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port)); - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); - } - - private Socket enableTLSv12OnSocket(Socket socket) { - if (socket instanceof SSLSocket) { - // TLS 1.2 is not enabled by default prior to Android 5.0. We must enable it - // explicitly to ensure we can communicate with GFE 3.20.4 which blocks TLS 1.0. - ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"}); - } - return socket; - } + public boolean sendSuperCmd(String cmdId) throws IOException, XmlPullParserException { + String xmlStr = openHttpConnectionToString(httpClientLongConnectNoReadTimeout, getHttpsUrl(true), "supercmd", "cmdId=" + cmdId); + return !getXmlString(xmlStr, "supercmd", true).equals("0"); } } diff --git a/app/src/main/java/com/limelight/utils/ServerHelper.java b/app/src/main/java/com/limelight/utils/ServerHelper.java index fc99612ce..dfc62e0e1 100644 --- a/app/src/main/java/com/limelight/utils/ServerHelper.java +++ b/app/src/main/java/com/limelight/utils/ServerHelper.java @@ -64,6 +64,9 @@ public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetai intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId()); intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid); intent.putExtra(Game.EXTRA_PC_NAME, computer.name); + if (app.getCmdList() != null) { + intent.putExtra(Game.EXTRA_APP_CMD, app.getCmdList().toString()); + } try { if (computer.serverCert != null) { intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());