diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 2913089216..a26ceda2ce 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { -d --select-usb --disable-screensaver --display-id= + --display-ime-policy= --display-orientation= -e --select-tcpip -f --fullscreen @@ -148,6 +149,10 @@ _scrcpy() { COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; + --display-ime-policy) + COMPREPLY=($(compgen -W 'local fallback_display hide' -- "$cur")) + return + ;; --record-orientation) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0897b9ccd0..03e6560a51 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -30,6 +30,7 @@ arguments=( {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display-id=[Specify the display id to mirror]' + '--display-ime-policy=[Sets the policy for how the display should show IME]' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 75bf608829..4cbec20d6c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -169,6 +169,16 @@ Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The num Default is 0. +.TP +.BI "\-\-display\-ime-policy " value +Sets the policy for how the display should show IME. + +Possible values are "local", "fallback_display" and "hide". + + - The "local" policy means that the IME should appear on the local display. + - The "fallback_display" policy means that the IME should appear on a fallback display. The fallback display is always DEFAULT DISPLAY. + - The "hide" policy means that the IME should be hidden. + .TP .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). diff --git a/app/src/cli.c b/app/src/cli.c index a2e6ab1aaa..00676bf9b5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -94,6 +94,7 @@ enum { OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, + OPT_DISPLAY_IME_POLICY, OPT_RECORD_ORIENTATION, OPT_ORIENTATION, OPT_KEYBOARD, @@ -366,6 +367,20 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_IME_POLICY, + .longopt = "display-ime-policy", + .argdesc = "value", + .text = "Sets the policy for how the display should show IME.\n" + "Possible values are \"local\", \"fallback_display\" and " + "\"hide\".\n" + "The \"local\" policy means that the IME should appear on the " + "local display.\n" + "The \"fallback_display\" policy means that the IME should " + "appear on a fallback display. The fallback display is always " + "DEFAULT DISPLAY.\n" + "The \"hide\" policy means that the IME should be hidden.\n", + }, { .longopt_id = OPT_DISPLAY_ORIENTATION, .longopt = "display-orientation", @@ -1614,6 +1629,24 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } +static bool +parse_display_ime_policy(const char *optarg, enum sc_display_ime_policy *policy) { + if (!strcmp(optarg, "local")) { + *policy = SC_DISPLAY_IME_POLICY_LOCAL; + return true; + } + if (!strcmp(optarg, "fallback_display")) { + *policy = SC_DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + return true; + } + if (!strcmp(optarg, "hide")) { + *policy = SC_DISPLAY_IME_POLICY_HIDE; + return true; + } + LOGE("Unsupported display IME policy: %s (expected local, fallback_display, hide)", optarg); + return false; +} + static bool parse_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -2722,6 +2755,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_VD_SYSTEM_DECORATIONS: opts->vd_system_decorations = false; break; + case OPT_DISPLAY_IME_POLICY: + if (!parse_display_ime_policy(optarg, &opts->display_ime_policy)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2978,6 +3016,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINDED) { + LOGE("--display-ime_policy is only available with --video-source=display"); + return false; + } + if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { LOGE("Cannot specify both --camera-id and --camera-facing"); return false; @@ -3014,9 +3057,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->display_id != 0 && opts->new_display) { - LOGE("Cannot specify both --display-id and --new-display"); - return false; + if (opts->display_id != 0) { + if (opts->new_display) { + LOGE("Cannot specify both --display-id and --new-display"); + return false; + } + } else { + if (!opts->new_display + && opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINDED) { + LOGE("--display-ime-policy not supported if display_id == 0 and new_display == NULL"); + return false; + } } if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { @@ -3200,6 +3251,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("OTG mode: could not select display"); return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINDED) { + LOGE("OTG mode: could not set the policy for how the display should show IME."); + return false; + } if (v4l2) { LOGE("OTG mode: could not sink to V4L2 device"); return false; diff --git a/app/src/options.c b/app/src/options.c index 044aa0149b..df02916a52 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -109,6 +109,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_dup = false, .new_display = NULL, .start_app = NULL, + .display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINDED, .angle = NULL, .vd_destroy_content = true, .vd_system_decorations = true, diff --git a/app/src/options.h b/app/src/options.h index c8425808ba..1c4690f419 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -83,6 +83,13 @@ enum sc_orientation { // v v v SC_ORIENTATION_FLIP_270, // 1 1 1 }; +enum sc_display_ime_policy { + SC_DISPLAY_IME_POLICY_UNDEFINDED = -1, + SC_DISPLAY_IME_POLICY_LOCAL, + SC_DISPLAY_IME_POLICY_FALLBACK_DISPLAY, + SC_DISPLAY_IME_POLICY_HIDE, +}; + enum sc_orientation_lock { SC_ORIENTATION_UNLOCKED, SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation @@ -309,6 +316,7 @@ struct scrcpy_options { bool audio_dup; const char *new_display; // [x][/] parsed by the server const char *start_app; + enum sc_display_ime_policy display_ime_policy; bool vd_destroy_content; bool vd_system_decorations; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 641d93f7f0..b3ff9b368d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -436,6 +436,7 @@ scrcpy(struct scrcpy_options *options) { .control = options->control, .display_id = options->display_id, .new_display = options->new_display, + .display_ime_policy = options->display_ime_policy, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, diff --git a/app/src/server.c b/app/src/server.c index cf181abc00..8b555a05cb 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -376,6 +376,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINDED) { + assert(params->display_ime_policy >= 0); + ADD_PARAM("display_ime_policy=%" PRIu32, params->display_ime_policy); + } if (!params->vd_destroy_content) { ADD_PARAM("vd_destroy_content=false"); } diff --git a/app/src/server.h b/app/src/server.h index a03689ff4f..5f4592de90 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -50,6 +50,7 @@ struct sc_server_params { bool control; uint32_t display_id; const char *new_display; + enum sc_display_ime_policy display_ime_policy; bool video; bool audio; bool audio_dup; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5d1673e869..9e0dfa8b40 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -61,3 +61,14 @@ To move them to the main display instead, use: ``` scrcpy --new-display --no-vd-destroy-content ``` + +## Display IME policy + +By default, virtual display's IME appears on the DEFAULT display. + +TO appears on the LOCAL display, use `--display-ime-policy=local`: + +```bash +scrcpy --display-id=1 --display-ime-policy=local +scrcpy --new-display --display-ime-policy=local +``` diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 49b23e819a..41c6e491bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.BatteryManager; import android.system.ErrnoException; @@ -100,15 +101,27 @@ private void runCleanUp(Options options) { boolean powerOffScreen = options.getPowerOffScreenOnClose(); int displayId = options.getDisplayId(); + int restoreDisplayImePolicy = -1; + if (displayId > 0) { + int displayImePolicy = options.getDisplayImePolicy(); + if (displayImePolicy != -1) { + int currentValue = ServiceManager.getWindowManager().getDisplayImePolicy(displayId); + if (currentValue != displayImePolicy) { + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy); + restoreDisplayImePolicy = currentValue; + } + } + } + try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout, restoreDisplayImePolicy); } catch (IOException e) { Ln.e("Clean up I/O exception", e); } } - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) - throws IOException { + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout, + int restoreDisplayImePolicy) throws IOException { String[] cmd = { "app_process", "/", @@ -118,6 +131,7 @@ private void run(int displayId, int restoreStayOn, boolean disableShowTouches, b String.valueOf(disableShowTouches), String.valueOf(powerOffScreen), String.valueOf(restoreScreenOffTimeout), + String.valueOf(restoreDisplayImePolicy), }; ProcessBuilder builder = new ProcessBuilder(cmd); @@ -178,6 +192,7 @@ public static void main(String... args) { boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean powerOffScreen = Boolean.parseBoolean(args[3]); int restoreScreenOffTimeout = Integer.parseInt(args[4]); + int restoreDisplayImePolicy = Integer.parseInt(args[5]); // Dynamic option boolean restoreDisplayPower = false; @@ -223,6 +238,11 @@ public static void main(String... args) { } } + if (restoreDisplayImePolicy != -1) { + Ln.i("Restoring \"display IME policy\""); + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, restoreDisplayImePolicy); + } + // Change the power of the main display when mirroring a virtual display int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; if (Device.isScreenOn(targetDisplayId)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 8a43875078..04cb3966c0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -48,6 +48,7 @@ public class Options { private boolean showTouches; private boolean stayAwake; private int screenOffTimeout = -1; + private int displayImePolicy = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -194,6 +195,10 @@ public List getAudioCodecOptions() { return audioCodecOptions; } + public int getDisplayImePolicy() { + return displayImePolicy; + } + public String getVideoEncoder() { return videoEncoder; } @@ -381,6 +386,14 @@ public static Options parse(String... args) { case "display_id": options.displayId = Integer.parseInt(value); break; + case "display_ime_policy": + if (!value.isEmpty()) { + options.displayImePolicy = Integer.parseInt(value); + if (options.displayImePolicy == -1) { + throw new IllegalArgumentException("Invalid display IME policy: " + options.displayImePolicy); + } + } + break; case "show_touches": options.showTouches = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eb8b533a64..09cfd6cf23 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -80,9 +80,15 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc throw new ConfigurationException("Camera mirroring is not supported"); } - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) { - Ln.e("New virtual display is not supported before Android 10"); - throw new ConfigurationException("New virtual display is not supported"); + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { + if (options.getNewDisplay() != null) { + Ln.e("New virtual display is not supported before Android 10"); + throw new ConfigurationException("New virtual display is not supported"); + } + if (options.getDisplayImePolicy() != -1) { + Ln.e("Display IME policy is not supported before Android 10"); + throw new ConfigurationException("Display IME policy is not supported"); + } } CleanUp cleanUp = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 033d6b9a8c..792b3a8a7a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -49,6 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; + private int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -68,6 +69,7 @@ public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.newDisplay = options.getNewDisplay(); assert newDisplay != null; this.maxSize = options.getMaxSize(); + this.displayImePolicy = options.getDisplayImePolicy(); this.crop = options.getCrop(); assert options.getCaptureOrientationLock() != null; this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; @@ -191,6 +193,10 @@ public void startNew(Surface surface) { virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + if (displayImePolicy != -1) { + ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); + } + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 04f5abd73a..c152530080 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,12 +4,18 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; +import android.os.Build; import android.os.IInterface; import android.view.IDisplayWindowListener; import java.lang.reflect.Method; public final class WindowManager { + // see https://android.googlesource.com/platform/frameworks/base.git/+/refs/heads/main/core/java/android/view/WindowManager.java#692 + public static final int DISPLAY_IME_POLICY_LOCAL = 0; + public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; + public static final int DISPLAY_IME_POLICY_HIDE = 2; + private final IInterface manager; private Method getRotationMethod; @@ -22,6 +28,10 @@ public final class WindowManager { private Method thawDisplayRotationMethod; private int thawDisplayRotationMethodVersion; + private Method getDisplayImePolicyMethod; + + private Method setDisplayImePolicyMethod; + static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); return new WindowManager(manager); @@ -198,4 +208,62 @@ public void unregisterDisplayWindowListener(IDisplayWindowListener listener) { Ln.e("Could not unregister display window listener", e); } } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getGetDisplayImePolicyMethod() throws NoSuchMethodException { + if (getDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + getDisplayImePolicyMethod = manager.getClass().getMethod("getDisplayImePolicy", int.class); + } else { + getDisplayImePolicyMethod = manager.getClass().getMethod("shouldShowIme", int.class); + } + } + return getDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public int getDisplayImePolicy(int displayId) { + try { + Method method = getGetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + return (int) method.invoke(manager, displayId); + } else { + boolean shouldShow = (boolean) method.invoke(manager, displayId); + return shouldShow ? DISPLAY_IME_POLICY_LOCAL : DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + } + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return -1; + } + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getSetDisplayImePolicyMethod() throws NoSuchMethodException { + if (setDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + setDisplayImePolicyMethod = manager.getClass().getMethod("setDisplayImePolicy", int.class, int.class); + } else { + setDisplayImePolicyMethod = manager.getClass().getMethod("setShouldShowIme", int.class, boolean.class); + } + } + return setDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public void setDisplayImePolicy(int displayId, int imePolicy) { + try { + Method method = getSetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + method.invoke(manager, displayId, imePolicy); + } else { + if (imePolicy != DISPLAY_IME_POLICY_HIDE) { + method.invoke(manager, displayId, imePolicy == DISPLAY_IME_POLICY_LOCAL); + } else { + Ln.w("DISPLAY_IME_POLICY_HIDE is not supported on this device"); + } + } + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + } + } }