Compare commits

...

11 Commits

Author SHA1 Message Date
Romain Vimont
63d848fc55 Fix PTS produced by the default OPUS encoder
The default OPUS encoder on Android rewrites the PTS so that it exactly
matches the number of samples.

As a consequence:
 - audio clock drift is not compensated
 - hard silences are ignored

To fix this behavior, recreate the PTS based on the current time (after
encoding) and the packet duration.
2025-03-02 18:00:12 +01:00
Romain Vimont
b292e356de Add audio sources 2025-03-02 17:17:01 +01:00
Romain Vimont
9fb7446b88 Refactor audio sources
Store the target audio source integer (one of the constants in
android.media.MediaRecorder.AudioSource) in the AudioSource enum (or -1
if not relevant).

This will simplify adding new audio sources.
2025-02-22 12:46:06 +01:00
Romain Vimont
671025cb68 Handle audio stream discontinuities
The audio regulator assumed a continuous audio stream. But some audio
sources (like the "voice call" audio source) do not produce any packets
on silence, breaking this assumption.

Use PTS to detect such discontinuities.

TODO: if PTS values are broken, the detection is also broken.
2025-02-22 12:46:06 +01:00
Romain Vimont
8925bdc8fd Report underflow samples in verbose mode
Report the number of silence samples inserted due to underflow every
second, along with the other metrics.
2025-02-22 12:26:27 +01:00
Romain Vimont
ea4c076345 Disable audio regulator underflow logs
Only enable them if SC_AUDIO_REGULATOR_DEBUG is set, as they may spam
the output.
2025-02-22 12:26:27 +01:00
Romain Vimont
d892a9aac5 Disable checkstyle line length warning
Checkstyle reports a warning because the line containing a long URL is
more than 150 characters. But we can't split the URL, so disable the
warning.
2025-02-22 12:22:45 +01:00
chengjian.scj
fd8bef68b7 Add --display-ime-policy option
Add an option to select where the IME should be displayed.

Possible values are "local", "fallback" and "hide".

PR #5703 <https://github.com/Genymobile/scrcpy/pull/5703>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-02-22 11:05:22 +01:00
Sam Listopad II
986328ff9e Allow controls with --no-window
Without a window, mouse and keyboard events may not be received, but
the control channel is still necessary for other features:

 * --turn-screen-off
 * --stay-awake
 * --show-touches
 * --power-off-on-close
 * --start-app

Fixes #5803 <https://github.com/Genymobile/scrcpy/issues/5803>
PR #5804 <https://github.com/Genymobile/scrcpy/pull/5804>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-01-31 10:11:17 +01:00
Simon Chan
c27d116a66 Fix AudioRecord package name for Android 16
Since commit 9f91a5eebb4520b9333576e946b3911d0f946a04 in frameworks/av
(AOSP), an AudioRecord can be created only if the declared package name
in the AttributionSource is "shell" (for the shell UID):
 - <7c4e6991ac/services/audiopolicy/permission/NativePermissionController.cpp (129)>
 - <7c4e6991ac/services/audiopolicy/permission/NativePermissionController.cpp (40)>

Refs <9f91a5eebb%5E%21/>
Fixes #5698 <https://github.com/Genymobile/scrcpy/issues/5698>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-12-23 19:41:25 +01:00
Romain Vimont
eac711ace6 Remove unused rotation and fold listeners
IRotationWatcher and IDisplayFoldListener are no longer used since
commit 39d51ff2cc.
2024-12-23 12:51:27 +01:00
28 changed files with 511 additions and 150 deletions

View File

@@ -23,6 +23,7 @@ _scrcpy() {
-d --select-usb
--disable-screensaver
--display-id=
--display-ime-policy=
--display-orientation=
-e --select-tcpip
-f --fullscreen
@@ -121,7 +122,7 @@ _scrcpy() {
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
COMPREPLY=($(compgen -W 'output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance' -- "$cur"))
return
;;
--camera-facing)
@@ -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 hide' -- "$cur"))
return
;;
--record-orientation)
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return

View File

@@ -16,7 +16,7 @@ arguments=(
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic playback)'
'--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)'
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--camera-ar=[Select the camera size by its aspect ratio]'
@@ -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[Set the policy for selecting where the IME should be displayed]'
'--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]'

View File

@@ -67,13 +67,19 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output, mic or playback).
Select the audio source. Possible values are:
The "output" source forwards the whole audio output, and disables playback on the device.
The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
The "mic" source captures the microphone.
- "output": forwards the whole audio output, and disables playback on the device.
- "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
- "mic": captures the microphone.
- "mic-unprocessed": captures the microphone unprocessed (raw) sound.
- "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available.
- "mic-voice-recognition": captures the microphone tuned for voice recognition.
- "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available).
- "voice-call": captures voice call.
- "voice-call-uplink": captures voice call uplink only.
- "voice-call-downlink": captures voice call downlink only.
- "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback.
Default is output.
@@ -161,6 +167,19 @@ The available display ids can be listed by \fB\-\-list\-displays\fR.
Default is 0.
.TP
.BI "\-\-display\-ime\-policy " value
Set the policy for selecting where the IME should be displayed.
Possible values are "local", "fallback" and "hide":
- "local" means that the IME should appear on the local display.
- "fallback" means that the IME should appear on a fallback display (the default display).
- "hide" means that the IME should be hidden.
By default, the IME policy is left unchanged.
.TP
.BI "\-\-display\-orientation " value
Set the initial display orientation.
@@ -389,7 +408,7 @@ Disable video playback on the computer.
.TP
.B \-\-no\-window
Disable scrcpy window. Implies --no-video-playback and --no-control.
Disable scrcpy window. Implies --no-video-playback.
.TP
.BI "\-\-orientation " value

View File

@@ -76,8 +76,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
// Wait until the buffer is filled up to at least target_buffering
// before playing
if (buffered_samples < ar->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", out_samples);
#endif
// Delay playback starting to reach the target buffering. Fill the
// whole buffer with silence (len is small compared to the
// arbitrary margin value).
@@ -98,8 +100,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
// dropped to keep the latency minimal. However, this would cause very
// audible glitches, so let the clock compensation restore the target
// latency.
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
#endif
memset(out + TO_BYTES(read), 0, TO_BYTES(silence));
bool received = atomic_load_explicit(&ar->received,
@@ -137,6 +141,35 @@ bool
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
SwrContext *swr_ctx = ar->swr_ctx;
uint32_t input_samples = frame->nb_samples;
assert(frame->pts >= 0);
int64_t pts = frame->pts;
if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) {
LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs",
pts - ar->next_expected_pts);
// More than 100ms: consider it as a discontinuity
// (typically because silence packets were not captured)
uint32_t can_read = sc_audiobuf_can_read(&ar->buf);
if (input_samples + can_read < ar->target_buffering) {
// Adjust buffering to the target value directly
uint32_t silence = ar->target_buffering - can_read - input_samples;
sc_audiobuf_write_silence(&ar->buf, silence);
}
// Reset state
ar->avg_buffering.avg = ar->target_buffering;
int ret = swr_set_compensation(swr_ctx, 0, 0);
assert(!ret); // disabling compensation should never fail
ar->compensation_active = false;
ar->samples_since_resync = 0;
atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed);
}
int64_t packet_duration = input_samples * INT64_C(1000000)
/ ar->sample_rate;
ar->next_expected_pts = pts + packet_duration;
int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same.
// Add more space (256) for clock compensation.
@@ -209,6 +242,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
if (played) {
underflow = atomic_exchange_explicit(&ar->underflow, 0,
memory_order_relaxed);
ar->underflow_report += underflow;
max_buffered_samples = ar->target_buffering * 11 / 10
+ 60 * ar->sample_rate / 1000 /* 60 ms */;
@@ -255,7 +289,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
}
// Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
int32_t instant_compensation = (int32_t) written - input_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
@@ -311,7 +345,9 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ar->target_buffering, avg, can_read, diff);
" compensation=%d (underflow=%" PRIu32 ")",
ar->target_buffering, avg, can_read, diff, ar->underflow_report);
ar->underflow_report = 0;
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
@@ -394,7 +430,9 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
atomic_init(&ar->played, false);
atomic_init(&ar->received, false);
atomic_init(&ar->underflow, 0);
ar->underflow_report = 0;
ar->compensation_active = false;
ar->next_expected_pts = 0;
return true;

View File

@@ -46,6 +46,9 @@ struct sc_audio_regulator {
// Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow;
// Number of silence samples inserted since the last log
uint32_t underflow_report;
// Non-zero compensation applied (only used by the receiver thread)
bool compensation_active;
@@ -54,6 +57,9 @@ struct sc_audio_regulator {
// Set to true the first time samples are pulled by the player
atomic_bool played;
// PTS of the next expected packet (useful to detect discontinuities)
int64_t next_expected_pts;
};
bool

View File

@@ -113,6 +113,7 @@ enum {
OPT_ANGLE,
OPT_NO_VD_SYSTEM_DECORATIONS,
OPT_NO_VD_DESTROY_CONTENT,
OPT_DISPLAY_IME_POLICY,
};
struct sc_option {
@@ -216,13 +217,31 @@ static const struct sc_option options[] = {
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output, mic or playback).\n"
"The \"output\" source forwards the whole audio output, and "
"disables playback on the device.\n"
"The \"playback\" source captures the audio playback (Android "
"apps can opt-out, so the whole output is not necessarily "
.text = "Select the audio source. Possible values are:\n"
" - \"output\": forwards the whole audio output, and disables "
"playback on the device.\n"
" - \"playback\": captures the audio playback (Android apps "
"can opt-out, so the whole output is not necessarily "
"captured).\n"
"The \"mic\" source captures the microphone.\n"
" - \"mic\": captures the microphone.\n"
" - \"mic-unprocessed\": captures the microphone unprocessed "
"(raw) sound.\n"
" - \"mic-camcorder\": captures the microphone tuned for video "
"recording, with the same orientation as the camera if "
"available.\n"
" - \"mic-voice-recognition\": captures the microphone tuned "
"for voice recognition.\n"
" - \"mic-voice-communication\": captures the microphone tuned "
"for voice communications (it will for instance take advantage "
"of echo cancellation or automatic gain control if "
"available).\n"
" - \"voice-call\": captures voice call.\n"
" - \"voice-call-uplink\": captures voice call uplink only.\n"
" - \"voice-call-downlink\": captures voice call downlink "
"only.\n"
" - \"voice-performance\": captures audio meant to be "
"processed for live performance (karaoke), includes both the "
"microphone and the device playback.\n"
"Default is output.",
},
{
@@ -366,6 +385,19 @@ 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 = "Set the policy for selecting where the IME should be "
"displayed.\n"
"Possible values are \"local\", \"fallback\" and \"hide\".\n"
"\"local\" means that the IME should appear on the local "
"display.\n"
"\"fallback\" means that the IME should appear on a fallback "
"display (the default display).\n"
"\"hide\" means that the IME should be hidden.",
},
{
.longopt_id = OPT_DISPLAY_ORIENTATION,
.longopt = "display-orientation",
@@ -689,8 +721,7 @@ static const struct sc_option options[] = {
{
.longopt_id = OPT_NO_WINDOW,
.longopt = "no-window",
.text = "Disable scrcpy window. Implies --no-video-playback and "
"--no-control.",
.text = "Disable scrcpy window. Implies --no-video-playback.",
},
{
.longopt_id = OPT_ORIENTATION,
@@ -1615,6 +1646,25 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_display_ime_policy(const char *s, enum sc_display_ime_policy *policy) {
if (!strcmp(s, "local")) {
*policy = SC_DISPLAY_IME_POLICY_LOCAL;
return true;
}
if (!strcmp(s, "fallback")) {
*policy = SC_DISPLAY_IME_POLICY_FALLBACK;
return true;
}
if (!strcmp(s, "hide")) {
*policy = SC_DISPLAY_IME_POLICY_HIDE;
return true;
}
LOGE("Unsupported display IME policy: %s (expected local, fallback or "
"hide)", s);
return false;
}
static bool
parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) {
@@ -2004,8 +2054,50 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
return true;
}
LOGE("Unsupported audio source: %s (expected output, mic or playback)",
optarg);
if (!strcmp(optarg, "mic-unprocessed")) {
*source = SC_AUDIO_SOURCE_MIC_UNPROCESSED;
return true;
}
if (!strcmp(optarg, "mic-camcorder")) {
*source = SC_AUDIO_SOURCE_MIC_CAMCORDER;
return true;
}
if (!strcmp(optarg, "mic-voice-recognition")) {
*source = SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION;
return true;
}
if (!strcmp(optarg, "mic-voice-communication")) {
*source = SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION;
return true;
}
if (!strcmp(optarg, "voice-call")) {
*source = SC_AUDIO_SOURCE_VOICE_CALL;
return true;
}
if (!strcmp(optarg, "voice-call-uplink")) {
*source = SC_AUDIO_SOURCE_VOICE_CALL_UPLINK;
return true;
}
if (!strcmp(optarg, "voice-call-downlink")) {
*source = SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK;
return true;
}
if (!strcmp(optarg, "voice-performance")) {
*source = SC_AUDIO_SOURCE_VOICE_PERFORMANCE;
return true;
}
LOGE("Unsupported audio source: %s (expected output, mic, playback, "
"mic-unprocessed, mic-camcorder, mic-voice-recognition, "
"mic-voice-communication, voice-call, voice-call-uplink, "
"voice-call-downlink, voice-performance)", optarg);
return false;
}
@@ -2723,6 +2815,12 @@ 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;
@@ -2761,9 +2859,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
#endif
if (!opts->window) {
// Without window, there cannot be any video playback or control
// Without window, there cannot be any video playback
opts->video_playback = false;
opts->control = false;
// Controls are still possible, allowing for options like
// --turn-screen-off
}
if (!opts->video) {
@@ -2978,6 +3077,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) {
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;
@@ -3019,6 +3124,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED
&& opts->display_id == 0 && !opts->new_display) {
LOGE("--display-ime-policy is only supported on a secondary display");
return false;
}
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {

View File

@@ -56,6 +56,7 @@ const struct scrcpy_options scrcpy_options_default = {
.capture_orientation_lock = SC_ORIENTATION_UNLOCKED,
.display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0,
.display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED,
.window_x = SC_WINDOW_POSITION_UNDEFINED,
.window_y = SC_WINDOW_POSITION_UNDEFINED,
.window_width = 0,

View File

@@ -59,6 +59,14 @@ enum sc_audio_source {
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
SC_AUDIO_SOURCE_PLAYBACK,
SC_AUDIO_SOURCE_MIC_UNPROCESSED,
SC_AUDIO_SOURCE_MIC_CAMCORDER,
SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION,
SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION,
SC_AUDIO_SOURCE_VOICE_CALL,
SC_AUDIO_SOURCE_VOICE_CALL_UPLINK,
SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK,
SC_AUDIO_SOURCE_VOICE_PERFORMANCE,
};
enum sc_camera_facing {
@@ -89,6 +97,13 @@ enum sc_orientation_lock {
SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation
};
enum sc_display_ime_policy {
SC_DISPLAY_IME_POLICY_UNDEFINED,
SC_DISPLAY_IME_POLICY_LOCAL,
SC_DISPLAY_IME_POLICY_FALLBACK,
SC_DISPLAY_IME_POLICY_HIDE,
};
static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7));
@@ -251,6 +266,7 @@ struct scrcpy_options {
enum sc_orientation_lock capture_orientation_lock;
enum sc_orientation display_orientation;
enum sc_orientation record_orientation;
enum sc_display_ime_policy display_ime_policy;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width;

View File

@@ -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,

View File

@@ -149,12 +149,43 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
return "mic";
case SC_AUDIO_SOURCE_PLAYBACK:
return "playback";
case SC_AUDIO_SOURCE_MIC_UNPROCESSED:
return "mic-unprocessed";
case SC_AUDIO_SOURCE_MIC_CAMCORDER:
return "mic-camcorder";
case SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION:
return "mic-voice-recognition";
case SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION:
return "mic-voice-communication";
case SC_AUDIO_SOURCE_VOICE_CALL:
return "voice-call";
case SC_AUDIO_SOURCE_VOICE_CALL_UPLINK:
return "voice-call-uplink";
case SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK:
return "voice-call-downlink";
case SC_AUDIO_SOURCE_VOICE_PERFORMANCE:
return "voice-performance";
default:
assert(!"unexpected audio source");
return NULL;
}
}
static const char *
sc_server_get_display_ime_policy_name(enum sc_display_ime_policy policy) {
switch (policy) {
case SC_DISPLAY_IME_POLICY_LOCAL:
return "local";
case SC_DISPLAY_IME_POLICY_FALLBACK:
return "fallback";
case SC_DISPLAY_IME_POLICY_HIDE:
return "hide";
default:
assert(!"unexpected display IME policy");
return NULL;
}
}
static bool
validate_string(const char *s) {
// The parameters values are passed as command line arguments to adb, so
@@ -376,6 +407,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_UNDEFINED) {
ADD_PARAM("display_ime_policy=%s",
sc_server_get_display_ime_policy_name(params->display_ime_policy));
}
if (!params->vd_destroy_content) {
ADD_PARAM("vd_destroy_content=false");
}

View File

@@ -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;

View File

@@ -116,3 +116,38 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
return samples_count;
}
uint32_t
sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) {
// Only the writer thread can write head, so memory_order_relaxed is
// sufficient
uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed);
// The tail cursor is updated after the data is consumed by the reader
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size;
if (!can_write) {
return 0;
}
if (samples_count > can_write) {
samples_count = can_write;
}
uint32_t right_count = buf->alloc_size - head;
if (right_count > samples_count) {
right_count = samples_count;
}
memset(buf->data + (head * buf->sample_size), 0,
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memset(buf->data, 0, left_count * buf->sample_size);
}
uint32_t new_head = (head + samples_count) % buf->alloc_size;
atomic_store_explicit(&buf->head, new_head, memory_order_release);
return samples_count;
}

View File

@@ -50,6 +50,9 @@ uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
uint32_t samples_count);
uint32_t
sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples);
static inline uint32_t
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
assert(buf->alloc_size);

View File

@@ -113,6 +113,14 @@ static void test_audiobuf_partial_read_write(void) {
uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3};
assert(!memcmp(data, expected2, 12));
w = sc_audiobuf_write_silence(&buf, 4);
assert(w == 4);
r = sc_audiobuf_read(&buf, data, 4);
assert(r == 4);
uint32_t expected3[] = {0, 0, 0, 0};
assert(!memcmp(data, expected3, 4));
sc_audiobuf_destroy(&buf);
}

View File

@@ -66,6 +66,20 @@ the computer:
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
```
Many sources are available:
- `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)).
- `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
- `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)).
- `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)).
- `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)).
- `mic-voice-recognition`: captures the microphone tuned for voice recognition (mapped to [`VOICE_RECOGNITION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_RECOGNITION)).
- `mic-voice-communication`: captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available) (mapped to [`VOICE_COMMUNICATION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION)).
- `voice-call`: captures voice call (mapped to [`VOICE_CALL`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL)).
- `voice-call-uplink`: captures voice call uplink only (mapped to [`VOICE_UPLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)).
- `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)).
- `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)).
### Duplication
An alternative device audio capture method is also available (only for Android

View File

@@ -61,3 +61,15 @@ To move them to the main display instead, use:
```
scrcpy --new-display --no-vd-destroy-content
```
## Display IME policy
By default, the virtual display IME appears on the default display.
To make it appear 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
```

View File

@@ -47,10 +47,8 @@ EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \
android/view/IDisplayWindowListener.aidl

View File

@@ -1,26 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
oneway interface IDisplayFoldListener
{
/** Called when the foldedness of a display changes */
void onDisplayFoldChanged(int displayId, boolean folded);
}

View File

@@ -1,25 +0,0 @@
/* //device/java/android/android/hardware/ISensorListener.aidl
**
** Copyright 2008, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
interface IRotationWatcher {
oneway void onRotationChanged(int rotation);
}

View File

@@ -4,6 +4,7 @@ import com.genymobile.scrcpy.device.Device;
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;
@@ -97,18 +98,31 @@ public final class CleanUp {
}
}
boolean powerOffScreen = options.getPowerOffScreenOnClose();
int displayId = options.getDisplayId();
int restoreDisplayImePolicy = -1;
if (displayId > 0) {
int displayImePolicy = options.getDisplayImePolicy();
if (displayImePolicy != -1) {
int currentDisplayImePolicy = ServiceManager.getWindowManager().getDisplayImePolicy(displayId);
if (currentDisplayImePolicy != displayImePolicy) {
ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy);
restoreDisplayImePolicy = currentDisplayImePolicy;
}
}
}
boolean powerOffScreen = options.getPowerOffScreenOnClose();
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 +132,7 @@ public final class CleanUp {
String.valueOf(disableShowTouches),
String.valueOf(powerOffScreen),
String.valueOf(restoreScreenOffTimeout),
String.valueOf(restoreDisplayImePolicy),
};
ProcessBuilder builder = new ProcessBuilder(cmd);
@@ -178,6 +193,7 @@ public final class CleanUp {
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 +239,11 @@ public final class CleanUp {
}
}
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)) {

View File

@@ -72,7 +72,7 @@ public final class FakeContext extends ContextWrapper {
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME);
builder.setPackageName("shell");
return builder.build();
}

View File

@@ -12,6 +12,7 @@ import com.genymobile.scrcpy.video.CameraAspectRatio;
import com.genymobile.scrcpy.video.CameraFacing;
import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.graphics.Rect;
import android.util.Pair;
@@ -48,6 +49,7 @@ public class Options {
private boolean showTouches;
private boolean stayAwake;
private int screenOffTimeout = -1;
private int displayImePolicy = -1;
private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions;
@@ -186,6 +188,10 @@ public class Options {
return screenOffTimeout;
}
public int getDisplayImePolicy() {
return displayImePolicy;
}
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
}
@@ -482,6 +488,9 @@ public class Options {
options.captureOrientationLock = pair.first;
options.captureOrientation = pair.second;
break;
case "display_ime_policy":
options.displayImePolicy = parseDisplayImePolicy(value);
break;
case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value);
break;
@@ -626,4 +635,17 @@ public class Options {
return Pair.create(lock, Orientation.getByName(value));
}
private static int parseDisplayImePolicy(String value) {
switch (value) {
case "local":
return WindowManager.DISPLAY_IME_POLICY_LOCAL;
case "fallback":
return WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
case "hide":
return WindowManager.DISPLAY_IME_POLICY_HIDE;
default:
throw new IllegalArgumentException("Invalid display IME policy: " + value);
}
}
}

View File

@@ -80,9 +80,15 @@ public final class Server {
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;

View File

@@ -12,7 +12,6 @@ import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
@@ -32,18 +31,7 @@ public class AudioDirectCapture implements AudioCapture {
private AudioRecordReader reader;
public AudioDirectCapture(AudioSource audioSource) {
this.audioSource = getAudioSourceValue(audioSource);
}
private static int getAudioSourceValue(AudioSource audioSource) {
switch (audioSource) {
case OUTPUT:
return MediaRecorder.AudioSource.REMOTE_SUBMIX;
case MIC:
return MediaRecorder.AudioSource.MIC;
default:
throw new IllegalArgumentException("Unsupported audio source: " + audioSource);
}
this.audioSource = audioSource.getDirectAudioSource();
}
@TargetApi(AndroidVersions.API_23_ANDROID_6_0)

View File

@@ -55,6 +55,9 @@ public final class AudioEncoder implements AsyncProcessor {
private final List<CodecOption> codecOptions;
private final String encoderName;
private boolean recreatePts;
private long previousPts;
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
// So many pending tasks would lead to an unacceptable delay anyway.
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
@@ -118,6 +121,9 @@ public final class AudioEncoder implements AsyncProcessor {
OutputTask task = outputTasks.take();
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
try {
if (recreatePts) {
fixTimestamp(task.bufferInfo);
}
streamer.writePacket(buffer, task.bufferInfo);
} finally {
mediaCodec.releaseOutputBuffer(task.index, false);
@@ -125,6 +131,24 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
private void fixTimestamp(MediaCodec.BufferInfo bufferInfo) {
assert recreatePts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// Config packet, nothing to fix
return;
}
long pts = bufferInfo.presentationTimeUs;
if (previousPts != 0) {
long now = System.nanoTime() / 1000;
long duration = pts - previousPts;
bufferInfo.presentationTimeUs = now - duration;
}
previousPts = pts;
}
@Override
public void start(TerminationListener listener) {
thread = new Thread(() -> {
@@ -194,6 +218,11 @@ public final class AudioEncoder implements AsyncProcessor {
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);
// The default OPUS encoder generates its own input PTS which matches the number of samples. This is not the behavior we want: it
// ignores any audio clock drift and hard silences (packets not produced on silence). To fix this behavior, regenerate PTS based on the
// current time and the packet duration.
recreatePts = "c2.android.opus.encoder".equals(mediaCodec.getName());
mediaCodecThread = new HandlerThread("media-codec");
mediaCodecThread.start();

View File

@@ -1,20 +1,38 @@
package com.genymobile.scrcpy.audio;
import android.annotation.SuppressLint;
import android.media.MediaRecorder;
@SuppressLint("InlinedApi")
public enum AudioSource {
OUTPUT("output"),
MIC("mic"),
PLAYBACK("playback");
OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX),
MIC("mic", MediaRecorder.AudioSource.MIC),
PLAYBACK("playback", -1),
MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED),
MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER),
MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION),
MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION),
VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL),
VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_CALL),
VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_CALL),
VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE);
private final String name;
private final int directAudioSource;
AudioSource(String name) {
AudioSource(String name, int directAudioSource) {
this.name = name;
this.directAudioSource = directAudioSource;
}
public boolean isDirect() {
return this != PLAYBACK;
}
public int getDirectAudioSource() {
return directAudioSource;
}
public static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {

View File

@@ -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 class NewDisplayCapture extends SurfaceCapture {
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 class NewDisplayCapture extends SurfaceCapture {
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);

View File

@@ -4,14 +4,20 @@ import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.IInterface;
import android.view.IDisplayFoldListener;
import android.view.IDisplayWindowListener;
import android.view.IRotationWatcher;
import java.lang.reflect.Method;
public final class WindowManager {
@SuppressWarnings("checkstyle:LineLength")
// <https://android.googlesource.com/platform/frameworks/base.git/+/2103ff441c66772c80c8560e322dcd9a45be7dcd/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;
@@ -24,6 +30,9 @@ 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);
@@ -182,52 +191,6 @@ public final class WindowManager {
}
}
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
try {
Class<?> cls = manager.getClass();
try {
// display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
} catch (NoSuchMethodException e) {
// old version
if (displayId != 0) {
Ln.e("Secondary display rotation not supported on this device");
return;
}
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
}
} catch (Exception e) {
Ln.e("Could not register rotation watcher", e);
}
}
public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) {
try {
manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher);
} catch (Exception e) {
Ln.e("Could not unregister rotation watcher", e);
}
}
@TargetApi(AndroidVersions.API_29_ANDROID_10)
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
Ln.e("Could not register display fold listener", e);
}
}
@TargetApi(AndroidVersions.API_29_ANDROID_10)
public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
Ln.e("Could not unregister display fold listener", e);
}
}
@TargetApi(AndroidVersions.API_30_ANDROID_11)
public int[] registerDisplayWindowListener(IDisplayWindowListener listener) {
try {
@@ -246,4 +209,59 @@ public final class WindowManager {
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);
}
boolean shouldShowIme = (boolean) method.invoke(manager, displayId);
return shouldShowIme ? 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 displayImePolicy) {
try {
Method method = getSetDisplayImePolicyMethod();
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
method.invoke(manager, displayId, displayImePolicy);
} else if (displayImePolicy != DISPLAY_IME_POLICY_HIDE) {
method.invoke(manager, displayId, displayImePolicy == DISPLAY_IME_POLICY_LOCAL);
} else {
Ln.w("DISPLAY_IME_POLICY_HIDE is not supported before Android 12");
}
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
}
}
}