Compare commits

...

35 Commits

Author SHA1 Message Date
746eaea556 Add missing clipboard workaround for IQOO device
The first part of the workaround fixed getPrimaryClip(). This part fixes
setPrimaryClip().

Fixes #4703 <https://github.com/Genymobile/scrcpy/issues/4703>
Refs 5ce8672ebc
Refs #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2024-02-26 08:36:18 +01:00
78a7e4f293 Use sc_ prefix for device sender 2024-02-23 20:07:52 +01:00
9858eff856 Fix device message deserialization checks
If any message is incomplete, the deserialization method must return
immediately.
2024-02-23 20:07:52 +01:00
9e22f3bf1c Replace unsigned char by uint8_t for buffers
For consistency.
2024-02-23 20:07:52 +01:00
25f1e703b7 Extract ControlChannel class
This prevents many components from depending on the whole
DesktopConnection.
2024-02-23 20:07:16 +01:00
a7cf4daf3b Avoid negative average buffering
The assumption that underflow and overbuffering are caused by jitter
(and that the delay between the producer and consumer will be caught up)
does not always hold.

For example, if the consumer does not consume at the expected rate (the
SDL callback is not called often enough, which is an audio output
issue), many samples will be dropped due to overbuffering, decreasing
the average buffering indefinitely.

Prevent the average buffering to become negative to limit the
consequences of an unexpected behavior.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:09:38 +01:00
c12fdf900f Minimize buffer underflow on starting
If playback starts too early, insert silence until the buffer is filled
up to at least target_buffering before playing.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:05:33 +01:00
4502126e3b Use early return to avoid additional indentation
PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:05:30 +01:00
dfa3f97a87 Fix audio player comment
PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:05:22 +01:00
edac4b8a9a Increase buffering level smoothness
The buffering level does not change continuously: it increases abruptly
when a packet is received, and decreases abruptly when an audio block is
consumed.

To estimate the buffering level, a rolling average is used.

To make the buffering more stable, increase the smoothness of this
rolling average. This decreases the risk of enabling audio compensation
due to an estimation error.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:03:10 +01:00
44abed5c68 Improve audio compensation thresholds
Use different thresholds for enabling and disabling compensation.

Concretely, enable compensation if the difference between the average
and the target buffering levels exceeds 4 ms (instead of 1 ms). This
avoids unnecessary compensation due to small noise in buffering level
estimation.

But keep a smaller threshold (1 ms) for disabling compensation, so that
the buffering level is restored closer to the target value. This avoids
to keep the actual level close to the compensation threshold.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:02:10 +01:00
cfa4f7e2f2 Replace locks by atomics in audio player
The audio output thread only reads samples from the buffer, and most of
the time, the audio receiver thread only writes samples to the buffer.
In these cases, using atomics avoids lock contention.

There are still corner cases where the audio receiver thread needs to
"read" samples (and drop them), so lock only in these cases.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:01:33 +01:00
d47ecef1b5 Limit buffering time value
This avoids unreasonable values which could lead to integer overflow.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 15:59:58 +01:00
9efa162949 Configure clean up actions dynamically
Some actions may be performed when scrcpy exits, currently:
 - disable "show touches"
 - restore "stay on while plugged in"
 - power off screen
 - restore "power mode" (to disable "turn screen off")

They are performed from a separate process so that they can be executed
even when scrcpy-server is killed (e.g. if the device is unplugged).

The clean up actions to perform were configured when scrcpy started.
Given that there is no method to read the current "power mode" in
Android, and that "turn screen off" can be applied at any time using an
scrcpy shortcut, there was no way to determine if "power mode" had to be
restored on exit. Therefore, it was always restored to "normal", even
when not necessary.

However, setting the "power mode" is quite fragile on some devices, and
may cause some issues, so it is preferable to call it only when
necessary (when "turn screen off" has actually been called).

For that purpose, make the scrcpy-server main process and the clean up
process communicate the actions to perform over a pipe (stdin/stdout),
so that they can be changed dynamically. In particular, when the power
mode is changed at runtime, notify the clean up process.

Refs 1beec99f82
Refs #4456 <https://github.com/Genymobile/scrcpy/issues/4456>
Refs #4624 <https://github.com/Genymobile/scrcpy/issues/4624>
PR #4649 <https://github.com/Genymobile/scrcpy/pull/4649>
2024-02-17 15:49:08 +01:00
be3f949aa5 Adapt to display API changes
The method SurfaceControl.createDisplay() has been removed in AOSP.

Use DisplayManager to create a VirtualDisplay object instead.

Fixes #4646 <https://github.com/Genymobile/scrcpy/issues/4646>
Fixes #4656 <https://github.com/Genymobile/scrcpy/issues/4656>
PR #4657 <https://github.com/Genymobile/scrcpy/pull/4657>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-02-10 10:01:22 +01:00
f7b4a18b43 Catch generic ReflectiveOperationException
This exception is a super-type of:
 - ClassNotFoundException
 - IllegalAccessException
 - InstantiationException
 - InvocationTargetException
 - NoSuchFieldException
 - NoSuchMethodException

Use it to simplify.
2024-02-10 10:00:28 +01:00
05b5deacad Move service managers creation
Create the service managers from each manager wrapper class rather than
from their getter in ServiceManager.

The way a wrapper retrieve the underlying service is an implementation
detail, and it must be consistent with the way it accesses it, so it is
better to write the creation in the wrapper.
2024-02-10 10:00:26 +01:00
d25cbc55f2 Remove unused field 2024-02-09 18:32:48 +01:00
3333e67452 Fix memory leak on error
Fixes #4636 <https://github.com/Genymobile/scrcpy/issues/4636>
2024-02-01 09:19:47 +01:00
7c53a29d72 Remove useless run script
This script was outdated and redundant with ./run.
2024-01-26 13:13:55 +01:00
5187f7254e Add another clipboard workaround for IQOO device
Fixes #4589 <https://github.com/Genymobile/scrcpy/issues/4589>
Refs 5ce8672ebc
Refs #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2024-01-17 10:15:00 +01:00
2ad93d1fc0 Fix scrcpy_otg() return value on error
The function now returns an enum scrcpy_exit_code, not a bool.
2024-01-15 22:01:19 +01:00
d067a11478 Do not power on if no video
Power on the device on start only if video capture is enabled.

Note that it only impacts display mirroring, since control is completely
disabled if video source is camera.

Refs 110b3a16f6
2024-01-07 21:12:39 +01:00
cd4056d0f3 Fix include formatting 2024-01-02 10:22:28 +01:00
6a58891e13 Use current time as initial timestamp on error
If the initial timestamp could not be retrieved, use the current time as
returned by System.nanoTime(). In practice, it is the same time base as
AudioRecord timestamps.

Fixes #4536 <https://github.com/Genymobile/scrcpy/issues/4536>
2023-12-16 20:17:33 +01:00
ec41896c85 Fix integer overflow for audio packet duration
The result is assigned to a long (64-bit signed integer), but the
intermediate multiplication was stored in an int (32-bit signed
integer).

This value is only used as a fallback when no timestamp could be
retrieved, that's why it did not cause too much harm so far.

Fixes #4536 <https://github.com/Genymobile/scrcpy/issues/4536>
2023-12-16 20:16:31 +01:00
4cd61b5a90 Fix checkstyle violation
Reported by checkstyle:

> [ant:checkstyle] [INFO]
> scrcpy/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java:48:
> Line is longer than 150 characters (found 167). [LineLength]
2023-12-16 20:12:58 +01:00
d2ed4510a7 Simulate tilt multitouch event by pressing Shift
PR #4529 <https://github.com/Genymobile/scrcpy/pull/4529>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-15 22:12:07 +01:00
604dfd7c6b Fix incorrect compgen usage
PR #4532 <https://github.com/Genymobile/scrcpy/pull/4532>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-14 17:08:19 +01:00
af69689ec1 Fix bash completion syntax
PR #4532 <https://github.com/Genymobile/scrcpy/pull/4532>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-14 17:07:20 +01:00
cbce42336d Fix manpage syntax
The '-' character must be escaped.

Fixes #4528 <https://github.com/Genymobile/scrcpy/issues/4528>
2023-12-13 13:42:57 +01:00
c9a4d2b38f Use up-to-date values on display fold change
When a display is folded or unfolded, the maxSize may have been updated
since the option was passed, and deviceSize must be updated.

Refs #4469 <https://github.com/Genymobile/scrcpy/pull/4469>
2023-12-04 08:50:12 +01:00
1beec99f82 Explicitly exit cleanup process
This avoids an internal crash reported in `adb logcat`.

Refs #4456 <https://github.com/Genymobile/scrcpy/pull/4456#issuecomment-1837427802>
2023-12-04 08:45:35 +01:00
5ce8672ebc Add clipboard workaround for IQOO device
Fixes #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2023-12-04 08:43:31 +01:00
3001f8a2d5 Adapt AudioRecord workaround to Android 14
Android 14 added a new int parameter "halInputFlags" to an internal
method:
<f6135d75db>

Fixes #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2023-12-03 18:01:11 +01:00
52 changed files with 1004 additions and 995 deletions

View File

@ -115,13 +115,12 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'front back external' -- "$cur")) COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
return return
;; ;;
--orientation --orientation|--display-orientation)
--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return return
;; ;;
--record-orientation) --record-orientation)
COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation) --lock-video-orientation)

View File

@ -34,8 +34,8 @@ src = [
'src/trait/frame_source.c', 'src/trait/frame_source.c',
'src/trait/packet_source.c', 'src/trait/packet_source.c',
'src/util/acksync.c', 'src/util/acksync.c',
'src/util/audiobuf.c',
'src/util/average.c', 'src/util/average.c',
'src/util/bytebuf.c',
'src/util/file.c', 'src/util/file.c',
'src/util/intmap.c', 'src/util/intmap.c',
'src/util/intr.c', 'src/util/intr.c',
@ -212,9 +212,10 @@ if get_option('buildtype') == 'debug'
['test_binary', [ ['test_binary', [
'tests/test_binary.c', 'tests/test_binary.c',
]], ]],
['test_bytebuf', [ ['test_audiobuf', [
'tests/test_bytebuf.c', 'tests/test_audiobuf.c',
'src/util/bytebuf.c', 'src/util/audiobuf.c',
'src/util/memory.c',
]], ]],
['test_cli', [ ['test_cli', [
'tests/test_cli.c', 'tests/test_cli.c',

View File

@ -124,7 +124,7 @@ Use USB device (if there is exactly one, like adb -d).
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.TP .TP
.BI "\-\-disable-screensaver" .BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running. Disable screensaver while scrcpy is running.
.TP .TP
@ -642,7 +642,11 @@ Enable/disable FPS counter (print frames/second in logs)
.TP .TP
.B Ctrl+click-and-move .B Ctrl+click-and-move
Pinch-to-zoom from the center of the screen Pinch-to-zoom and rotate from the center of the screen
.TP
.B Shift+click-and-move
Tilt (slide vertically with two fingers)
.TP .TP
.B Drag & drop APK file .B Drag & drop APK file

View File

@ -458,6 +458,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
// in the buffer in a single pass // in the buffer in a single pass
LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " LOGW("Result of \"adb devices -l\" does not fit in 64Kb. "
"Please report an issue."); "Please report an issue.");
free(buf);
return false; return false;
} }

View File

@ -66,8 +66,7 @@ static void SDLCALL
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata; struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_AudioDeviceLock(), so // This callback is called with the lock used by SDL_LockAudioDevice()
// the audiobuf is protected
assert(len_int > 0); assert(len_int > 0);
size_t len = len_int; size_t len = len_int;
@ -77,12 +76,12 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
#endif #endif
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (!ap->played) { if (!played) {
// Part of the buffering is handled by inserting initial silence. The uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
// remaining (margin) last samples will be handled by compensation. // Wait until the buffer is filled up to at least target_buffering
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms // before playing
if (buffered_samples + margin < ap->target_buffering) { if (buffered_samples < ap->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", count); " samples", count);
// Delay playback starting to reach the target buffering. Fill the // Delay playback starting to reach the target buffering. Fill the
@ -93,10 +92,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
} }
} }
uint32_t read = MIN(buffered_samples, count); uint32_t read = sc_audiobuf_read(&ap->buf, stream, count);
if (read) {
sc_audiobuf_read(&ap->buf, stream, read);
}
if (read < count) { if (read < count) {
uint32_t silence = count - read; uint32_t silence = count - read;
@ -109,13 +105,16 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
silence); silence);
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
if (ap->received) { bool received = atomic_load_explicit(&ap->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering // Inserting additional samples immediately increases buffering
ap->underflow += silence; atomic_fetch_add_explicit(&ap->underflow, silence,
memory_order_relaxed);
} }
} }
ap->played = true; atomic_store_explicit(&ap->played, true, memory_order_relaxed);
} }
static uint8_t * static uint8_t *
@ -162,155 +161,168 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// swr_convert() returns the number of samples which would have been // swr_convert() returns the number of samples which would have been
// written if the buffer was big enough. // written if the buffer was big enough.
uint32_t samples_written = MIN(ret, dst_nb_samples); uint32_t samples = MIN(ret, dst_nb_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif #endif
// Since this function is the only writer, the current available space is uint32_t cap = sc_audiobuf_capacity(&ap->buf);
// at least the previous available space. In practice, it should almost if (samples > cap) {
// always be possible to write without lock. // Very very unlikely: a single resampled frame should never
bool lockless_write = samples_written <= ap->previous_can_write; // exceed the audio buffer size (or something is very wrong).
if (lockless_write) { // Ignore the first bytes in swr_buf to avoid memory corruption anyway.
sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); swr_buf += TO_BYTES(samples - cap);
samples = cap;
} }
SDL_LockAudioDevice(ap->device); uint32_t skipped_samples = 0;
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
if (lockless_write) { // All samples that could be written without locking have been written,
sc_audiobuf_commit_write(&ap->buf, samples_written); // now we need to lock to drop/consume old samples
} else { SDL_LockAudioDevice(ap->device);
uint32_t can_write = sc_audiobuf_can_write(&ap->buf);
if (samples_written > can_write) {
// Entering this branch is very unlikely, the audio buffer is
// allocated with a size sufficient to store 1 second more than the
// target buffering. If this happens, though, we have to skip old
// samples.
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
if (samples_written > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Ignore the first bytes in swr_buf
swr_buf += TO_BYTES(samples_written - cap);
// This change in samples_written will impact the
// instant_compensation below
samples_written = cap;
}
assert(samples_written >= can_write); // Retry with the lock
if (samples_written > can_write) { written += sc_audiobuf_write(&ap->buf,
uint32_t skip_samples = samples_written - can_write; swr_buf + TO_BYTES(written),
assert(buffered_samples >= skip_samples); remaining);
sc_audiobuf_skip(&ap->buf, skip_samples); if (written < samples) {
buffered_samples -= skip_samples; remaining = samples - written;
if (ap->played) { // Still insufficient, drop old samples to make space
// Dropping input samples instantly decreases buffering skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
ap->avg_buffering.avg -= skip_samples; assert(skipped_samples == remaining);
}
}
// It should remain exactly the expected size to write the new // Now there is enough space
// samples. uint32_t w = sc_audiobuf_write(&ap->buf,
assert(sc_audiobuf_can_write(&ap->buf) == samples_written); swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
(void) w;
} }
sc_audiobuf_write(&ap->buf, swr_buf, samples_written); SDL_UnlockAudioDevice(ap->device);
} }
buffered_samples += samples_written; uint32_t underflow = 0;
assert(buffered_samples == sc_audiobuf_can_read(&ap->buf)); uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
// Read with lock held, to be used after unlocking
bool played = ap->played;
uint32_t underflow = ap->underflow;
if (played) { if (played) {
uint32_t max_buffered_samples = ap->target_buffering underflow = atomic_exchange_explicit(&ap->underflow, 0,
+ 12 * ap->output_buffer memory_order_relaxed);
+ ap->target_buffering / 10;
if (buffered_samples > max_buffered_samples) {
uint32_t skip_samples = buffered_samples - max_buffered_samples;
sc_audiobuf_skip(&ap->buf, skip_samples);
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
}
// reset (the current value was copied to a local variable) max_buffered_samples = ap->target_buffering
ap->underflow = 0; + 12 * ap->output_buffer
+ ap->target_buffering / 10;
} else { } else {
// SDL playback not started yet, do not accumulate more than // SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay // max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start. // (and glitches to compensate) on start.
uint32_t max_initial_buffering = ap->target_buffering max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer;
+ 2 * ap->output_buffer; }
if (buffered_samples > max_initial_buffering) {
uint32_t skip_samples = buffered_samples - max_initial_buffering; uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
sc_audiobuf_skip(&ap->buf, skip_samples); if (can_read > max_buffered_samples) {
uint32_t skip_samples = 0;
SDL_LockAudioDevice(ap->device);
can_read = sc_audiobuf_can_read(&ap->buf);
if (can_read > max_buffered_samples) {
skip_samples = can_read - max_buffered_samples;
uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples);
assert(r == skip_samples);
(void) r;
skipped_samples += skip_samples;
}
SDL_UnlockAudioDevice(ap->device);
if (skip_samples) {
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", } else {
skip_samples); LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif #endif
}
} }
} }
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); atomic_store_explicit(&ap->received, true, memory_order_relaxed);
ap->received = true; if (!played) {
// Nothing more to do
return true;
}
SDL_UnlockAudioDevice(ap->device); // Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
int32_t dropped = (int32_t) skipped_samples;
if (played) { // The compensation must apply instantly, it must not be smoothed
// Number of samples added (or removed, if negative) for compensation ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
int32_t instant_compensation = if (ap->avg_buffering.avg < 0) {
(int32_t) samples_written - frame->nb_samples; // Since dropping samples instantly reduces buffering, the difference
int32_t inserted_silence = (int32_t) underflow; // is applied immediately to the average value, assuming that the delay
// between the producer and the consumer will be caught up.
//
// However, when this assumption is not valid, the average buffering
// may decrease indefinitely. Prevent it to become negative to limit
// the consequences.
ap->avg_buffering.avg = 0;
}
// The compensation must apply instantly, it must not be smoothed // However, the buffering level must be smoothed
ap->avg_buffering.avg += instant_compensation + inserted_silence; sc_average_push(&ap->avg_buffering, can_read);
// However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, buffered_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
buffered_samples, sc_average_get(&ap->avg_buffering)); can_read, sc_average_get(&ap->avg_buffering));
#endif #endif
ap->samples_since_resync += samples_written; ap->samples_since_resync += written;
if (ap->samples_since_resync >= ap->sample_rate) { if (ap->samples_since_resync >= ap->sample_rate) {
// Recompute compensation every second // Recompute compensation every second
ap->samples_since_resync = 0; ap->samples_since_resync = 0;
float avg = sc_average_get(&ap->avg_buffering); float avg = sc_average_get(&ap->avg_buffering);
int diff = ap->target_buffering - avg; int diff = ap->target_buffering - avg;
if (abs(diff) < (int) ap->sample_rate / 1000) {
// Do not compensate for less than 1ms, the error is just noise
diff = 0;
} else if (diff < 0 && buffered_samples < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below
// the average, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after
// 1 second)
int distance = 4 * ap->sample_rate;
// Limit compensation rate to 2%
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", ap->target_buffering, avg,
buffered_samples, diff);
if (diff != ap->compensation) { // Enable compensation when the difference exceeds +/- 4ms.
int ret = swr_set_compensation(swr_ctx, diff, distance); // Disable compensation when the difference is lower than +/- 1ms.
if (ret < 0) { int threshold = ap->compensation != 0
LOGW("Resampling compensation failed: %d", ret); ? ap->sample_rate / 1000 /* 1ms */
// not fatal : ap->sample_rate * 4 / 1000; /* 4ms */
} else {
ap->compensation = diff; if (abs(diff) < threshold) {
} // Do not compensate for small values, the error is just noise
diff = 0;
} else if (diff < 0 && can_read < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below the
// target, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after 1
// second)
int distance = 4 * ap->sample_rate;
// Limit compensation rate to 2%
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", ap->target_buffering, avg, can_read, diff);
if (diff != ap->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ap->compensation = diff;
} }
} }
} }
@ -397,7 +409,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
// producer and the consumer. It's too big on purpose, to guarantee that // producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel // the producer and the consumer will be able to access it in parallel
// without locking. // without locking.
size_t audiobuf_samples = ap->target_buffering + ap->sample_rate; uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
@ -413,16 +425,15 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
} }
ap->swr_buf_alloc_size = initial_swr_buf_size; ap->swr_buf_alloc_size = initial_swr_buf_size;
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
// Samples are produced and consumed by blocks, so the buffering must be // Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value. // smoothed to get a relatively stable value.
sc_average_init(&ap->avg_buffering, 32); sc_average_init(&ap->avg_buffering, 128);
ap->samples_since_resync = 0; ap->samples_since_resync = 0;
ap->received = false; ap->received = false;
ap->played = false; atomic_init(&ap->played, false);
ap->underflow = 0; atomic_init(&ap->received, false);
atomic_init(&ap->underflow, 0);
ap->compensation = 0; ap->compensation = 0;
// The thread calling open() is the thread calling push(), which fills the // The thread calling open() is the thread calling push(), which fills the

View File

@ -3,17 +3,18 @@
#include "common.h" #include "common.h"
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include "trait/frame_sink.h"
#include <util/audiobuf.h>
#include <util/average.h>
#include <util/thread.h>
#include <util/tick.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libswresample/swresample.h> #include <libswresample/swresample.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "trait/frame_sink.h"
#include "util/audiobuf.h"
#include "util/average.h"
#include "util/thread.h"
#include "util/tick.h"
struct sc_audio_player { struct sc_audio_player {
struct sc_frame_sink frame_sink; struct sc_frame_sink frame_sink;
@ -32,13 +33,9 @@ struct sc_audio_player {
uint16_t output_buffer; uint16_t output_buffer;
// Audio buffer to communicate between the receiver and the SDL audio // Audio buffer to communicate between the receiver and the SDL audio
// callback (protected by SDL_AudioDeviceLock()) // callback
struct sc_audiobuf buf; struct sc_audiobuf buf;
// The previous empty space in the buffer (only used by the receiver
// thread)
uint32_t previous_can_write;
// Resampler (only used from the receiver thread) // Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx; struct SwrContext *swr_ctx;
@ -47,7 +44,7 @@ struct sc_audio_player {
// The number of channels is the same for input and output // The number of channels is the same for input and output
unsigned nb_channels; unsigned nb_channels;
// The number of bytes per sample for a single channel // The number of bytes per sample for a single channel
unsigned out_bytes_per_sample; size_t out_bytes_per_sample;
// Target buffer for resampling (only used by the receiver thread) // Target buffer for resampling (only used by the receiver thread)
uint8_t *swr_buf; uint8_t *swr_buf;
@ -61,19 +58,16 @@ struct sc_audio_player {
uint32_t samples_since_resync; uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet // Number of silence samples inserted since the last received packet
// (protected by SDL_AudioDeviceLock()) atomic_uint_least32_t underflow;
uint32_t underflow;
// Current applied compensation value (only used by the receiver thread) // Current applied compensation value (only used by the receiver thread)
int compensation; int compensation;
// Set to true the first time a sample is received (protected by // Set to true the first time a sample is received
// SDL_AudioDeviceLock()) atomic_bool received;
bool received;
// Set to true the first time the SDL callback is called (protected by // Set to true the first time the SDL callback is called
// SDL_AudioDeviceLock()) atomic_bool played;
bool played;
const struct sc_audio_player_callbacks *cbs; const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata; void *cbs_userdata;

View File

@ -947,7 +947,11 @@ static const struct sc_shortcut shortcuts[] = {
}, },
{ {
.shortcuts = { "Ctrl+click-and-move" }, .shortcuts = { "Ctrl+click-and-move" },
.text = "Pinch-to-zoom from the center of the screen", .text = "Pinch-to-zoom and rotate from the center of the screen",
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt (slide vertically with two fingers)",
}, },
{ {
.shortcuts = { "Drag & drop APK file" }, .shortcuts = { "Drag & drop APK file" },
@ -1381,7 +1385,11 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
static bool static bool
parse_buffering_time(const char *s, sc_tick *tick) { parse_buffering_time(const char *s, sc_tick *tick) {
long value; long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, // In practice, buffering time should not exceed a few seconds.
// Limit it to some arbitrary value (1 hour) to prevent 32-bit overflow
// when multiplied by the audio sample size and the number of samples per
// millisecond.
bool ok = parse_integer_arg(s, &value, false, 0, 60 * 60 * 1000,
"buffering time"); "buffering time");
if (!ok) { if (!ok) {
return false; return false;
@ -2394,6 +2402,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (!opts->video) { if (!opts->video) {
opts->video_playback = false; opts->video_playback = false;
// Do not power on the device on start if video capture is disabled
opts->power_on = false;
} }
if (!opts->audio) { if (!opts->audio) {

View File

@ -87,7 +87,7 @@ write_position(uint8_t *buf, const struct sc_position *position) {
// write length (4 bytes) + string (non null-terminated) // write length (4 bytes) + string (non null-terminated)
static size_t static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) { write_string(const char *utf8, size_t max_len, uint8_t *buf) {
size_t len = sc_str_utf8_truncation_index(utf8, max_len); size_t len = sc_str_utf8_truncation_index(utf8, max_len);
sc_write32be(buf, len); sc_write32be(buf, len);
memcpy(&buf[4], utf8, len); memcpy(&buf[4], utf8, len);
@ -95,7 +95,7 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
} }
size_t size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
buf[0] = msg->type; buf[0] = msg->type;
switch (msg->type) { switch (msg->type) {
case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE:

View File

@ -98,7 +98,7 @@ struct sc_control_msg {
// buf size must be at least CONTROL_MSG_MAX_SIZE // buf size must be at least CONTROL_MSG_MAX_SIZE
// return the number of bytes written // return the number of bytes written
size_t size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf); sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf);
void void
sc_control_msg_log(const struct sc_control_msg *msg); sc_control_msg_log(const struct sc_control_msg *msg);

View File

@ -84,7 +84,7 @@ sc_controller_push_msg(struct sc_controller *controller,
static bool static bool
process_msg(struct sc_controller *controller, process_msg(struct sc_controller *controller,
const struct sc_control_msg *msg) { const struct sc_control_msg *msg) {
static unsigned char serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE];
size_t length = sc_control_msg_serialize(msg, serialized_msg); size_t length = sc_control_msg_serialize(msg, serialized_msg);
if (!length) { if (!length) {
return false; return false;

View File

@ -8,19 +8,22 @@
#include "util/log.h" #include "util/log.h"
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, sc_device_msg_deserialize(const uint8_t *buf, size_t len,
struct device_msg *msg) { struct sc_device_msg *msg) {
if (len < 5) { if (!len) {
// at least type + empty string length return 0; // no message
return 0; // not available
} }
msg->type = buf[0]; msg->type = buf[0];
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: { case DEVICE_MSG_TYPE_CLIPBOARD: {
if (len < 5) {
// at least type + empty string length
return 0; // no complete message
}
size_t clipboard_len = sc_read32be(&buf[1]); size_t clipboard_len = sc_read32be(&buf[1]);
if (clipboard_len > len - 5) { if (clipboard_len > len - 5) {
return 0; // not available return 0; // no complete message
} }
char *text = malloc(clipboard_len + 1); char *text = malloc(clipboard_len + 1);
if (!text) { if (!text) {
@ -36,6 +39,9 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
return 5 + clipboard_len; return 5 + clipboard_len;
} }
case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { case DEVICE_MSG_TYPE_ACK_CLIPBOARD: {
if (len < 9) {
return 0; // no complete message
}
uint64_t sequence = sc_read64be(&buf[1]); uint64_t sequence = sc_read64be(&buf[1]);
msg->ack_clipboard.sequence = sequence; msg->ack_clipboard.sequence = sequence;
return 9; return 9;
@ -47,7 +53,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
} }
void void
device_msg_destroy(struct device_msg *msg) { sc_device_msg_destroy(struct sc_device_msg *msg) {
if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) {
free(msg->clipboard.text); free(msg->clipboard.text);
} }

View File

@ -11,13 +11,13 @@
// type: 1 byte; length: 4 bytes // type: 1 byte; length: 4 bytes
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
enum device_msg_type { enum sc_device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,
DEVICE_MSG_TYPE_ACK_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD,
}; };
struct device_msg { struct sc_device_msg {
enum device_msg_type type; enum sc_device_msg_type type;
union { union {
struct { struct {
char *text; // owned, to be freed by free() char *text; // owned, to be freed by free()
@ -30,10 +30,10 @@ struct device_msg {
// return the number of bytes consumed (0 for no msg available, -1 on error) // return the number of bytes consumed (0 for no msg available, -1 on error)
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, sc_device_msg_deserialize(const uint8_t *buf, size_t len,
struct device_msg *msg); struct sc_device_msg *msg);
void void
device_msg_destroy(struct device_msg *msg); sc_device_msg_destroy(struct sc_device_msg *msg);
#endif #endif

View File

@ -76,6 +76,8 @@ sc_input_manager_init(struct sc_input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count; im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false; im->vfinger_down = false;
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->last_keycode = SDLK_UNKNOWN; im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0; im->last_mod = 0;
@ -347,9 +349,14 @@ simulate_virtual_finger(struct sc_input_manager *im,
} }
static struct sc_point static struct sc_point
inverse_point(struct sc_point point, struct sc_size size) { inverse_point(struct sc_point point, struct sc_size size,
point.x = size.width - point.x; bool invert_x, bool invert_y) {
point.y = size.height - point.y; if (invert_x) {
point.x = size.width - point.x;
}
if (invert_y) {
point.y = size.height - point.y;
}
return point; return point;
} }
@ -605,7 +612,9 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
im->vfinger_invert_x,
im->vfinger_invert_y);
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
} }
} }
@ -726,7 +735,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
return; return;
} }
// Pinch-to-zoom simulation. // Pinch-to-zoom, rotate and tilt simulation.
// //
// If Ctrl is hold when the left-click button is pressed, then // If Ctrl is hold when the left-click button is pressed, then
// pinch-to-zoom mode is enabled: on every mouse event until the left-click // pinch-to-zoom mode is enabled: on every mouse event until the left-click
@ -735,14 +744,29 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// //
// In other words, the center of the rotation/scaling is the center of the // In other words, the center of the rotation/scaling is the center of the
// screen. // screen.
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) //
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
// can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
const SDL_Keymod keymod = SDL_GetModState();
const bool ctrl_pressed = keymod & KMOD_CTRL;
const bool shift_pressed = keymod & KMOD_SHIFT;
if (event->button == SDL_BUTTON_LEFT && if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && CTRL_PRESSED) || ((down && !im->vfinger_down &&
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) { (!down && im->vfinger_down))) {
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); if (down) {
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
im->vfinger_invert_y = ctrl_pressed;
}
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
im->vfinger_invert_x,
im->vfinger_invert_y);
enum android_motionevent_action action = down enum android_motionevent_action action = down
? AMOTION_EVENT_ACTION_DOWN ? AMOTION_EVENT_ACTION_DOWN
: AMOTION_EVENT_ACTION_UP; : AMOTION_EVENT_ACTION_UP;

View File

@ -32,6 +32,8 @@ struct sc_input_manager {
} sdl_shortcut_mods; } sdl_shortcut_mods;
bool vfinger_down; bool vfinger_down;
bool vfinger_invert_x;
bool vfinger_invert_y;
// Tracks the number of identical consecutive shortcut key down events. // Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of // Not to be confused with event->repeat, which counts the number of

View File

@ -1,6 +1,7 @@
#include "receiver.h" #include "receiver.h"
#include <assert.h> #include <assert.h>
#include <stdint.h>
#include <SDL2/SDL_clipboard.h> #include <SDL2/SDL_clipboard.h>
#include "device_msg.h" #include "device_msg.h"
@ -26,7 +27,7 @@ sc_receiver_destroy(struct sc_receiver *receiver) {
} }
static void static void
process_msg(struct sc_receiver *receiver, struct device_msg *msg) { process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: { case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText(); char *current = SDL_GetClipboardText();
@ -51,11 +52,11 @@ process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
} }
static ssize_t static ssize_t
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) { process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) {
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); ssize_t r = sc_device_msg_deserialize(&buf[head], len - head, &msg);
if (r == -1) { if (r == -1) {
return -1; return -1;
} }
@ -64,7 +65,7 @@ process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len)
} }
process_msg(receiver, &msg); process_msg(receiver, &msg);
device_msg_destroy(&msg); sc_device_msg_destroy(&msg);
head += r; head += r;
assert(head <= len); assert(head <= len);
@ -78,7 +79,7 @@ static int
run_receiver(void *data) { run_receiver(void *data) {
struct sc_receiver *receiver = data; struct sc_receiver *receiver = data;
static unsigned char buf[DEVICE_MSG_MAX_SIZE]; static uint8_t buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {

View File

@ -498,7 +498,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
static bool static bool
device_read_info(struct sc_intr *intr, sc_socket device_socket, device_read_info(struct sc_intr *intr, sc_socket device_socket,
struct sc_server_info *info) { struct sc_server_info *info) {
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH]; uint8_t buf[SC_DEVICE_NAME_FIELD_LENGTH];
ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
if (r < SC_DEVICE_NAME_FIELD_LENGTH) { if (r < SC_DEVICE_NAME_FIELD_LENGTH) {
LOGE("Could not retrieve device information"); LOGE("Could not retrieve device information");

View File

@ -113,7 +113,7 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id,
static bool static bool
sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
const unsigned char *report_desc, const uint8_t *report_desc,
uint16_t report_desc_size) { uint16_t report_desc_size) {
uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; uint8_t request = ACCESSORY_SET_HID_REPORT_DESC;
@ -150,7 +150,7 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
bool bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const unsigned char *report_desc, uint16_t report_desc_size) { const uint8_t *report_desc, uint16_t report_desc_size) {
bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size);
if (!ok) { if (!ok) {
return false; return false;

View File

@ -57,7 +57,7 @@ sc_aoa_join(struct sc_aoa *aoa);
bool bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const unsigned char *report_desc, uint16_t report_desc_size); const uint8_t *report_desc, uint16_t report_desc_size);
bool bool
sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id);

View File

@ -62,7 +62,7 @@ scrcpy_otg(struct scrcpy_options *options) {
// Minimal SDL initialization // Minimal SDL initialization
if (SDL_Init(SDL_INIT_EVENTS)) { if (SDL_Init(SDL_INIT_EVENTS)) {
LOGE("Could not initialize SDL: %s", SDL_GetError()); LOGE("Could not initialize SDL: %s", SDL_GetError());
return false; return SCRCPY_EXIT_FAILURE;
} }
atexit(SDL_Quit); atexit(SDL_Quit);

112
app/src/util/audiobuf.c Normal file
View File

@ -0,0 +1,112 @@
#include "audiobuf.h"
#include <stdlib.h>
#include <string.h>
#include <util/log.h>
#include <util/memory.h>
bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) {
assert(sample_size);
assert(capacity);
// The actual capacity is (alloc_size - 1) so that head == tail is
// non-ambiguous
buf->alloc_size = capacity + 1;
buf->data = sc_allocarray(buf->alloc_size, sample_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->sample_size = sample_size;
atomic_init(&buf->head, 0);
atomic_init(&buf->tail, 0);
return true;
}
void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
free(buf->data);
}
uint32_t
sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) {
assert(samples_count);
uint8_t *to = to_;
// Only the reader thread can write tail without synchronization, so
// memory_order_relaxed is sufficient
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed);
// The head cursor is updated after the data is written to the array
uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size;
if (samples_count > can_read) {
samples_count = can_read;
}
if (to) {
uint32_t right_count = buf->alloc_size - tail;
if (right_count > samples_count) {
right_count = samples_count;
}
memcpy(to,
buf->data + (tail * buf->sample_size),
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memcpy(to + (right_count * buf->sample_size),
buf->data,
left_count * buf->sample_size);
}
}
uint32_t new_tail = (tail + samples_count) % buf->alloc_size;
atomic_store_explicit(&buf->tail, new_tail, memory_order_release);
return samples_count;
}
uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
uint32_t samples_count) {
const uint8_t *from = from_;
// 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 (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;
}
memcpy(buf->data + (head * buf->sample_size),
from,
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memcpy(buf->data,
from + (right_count * buf->sample_size),
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

@ -3,19 +3,25 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "util/bytebuf.h"
/** /**
* Wrapper around bytebuf to read and write samples * Wrapper around bytebuf to read and write samples
* *
* Each sample takes sample_size bytes. * Each sample takes sample_size bytes.
*/ */
struct sc_audiobuf { struct sc_audiobuf {
struct sc_bytebuf buf; uint8_t *data;
uint32_t alloc_size; // in samples
size_t sample_size; size_t sample_size;
atomic_uint_least32_t head; // writer cursor, in samples
atomic_uint_least32_t tail; // reader cursor, in samples
// empty: tail == head
// full: ((tail + 1) % alloc_size) == head
}; };
static inline uint32_t static inline uint32_t
@ -29,66 +35,31 @@ sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) {
return samples * buf->sample_size; return samples * buf->sample_size;
} }
static inline bool bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) { uint32_t capacity);
buf->sample_size = sample_size;
return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1);
}
static inline void void
sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) { sc_audiobuf_destroy(struct sc_audiobuf *buf);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_read(&buf->buf, to, bytes);
}
static inline void uint32_t
sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) { sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_skip(&buf->buf, bytes);
}
static inline void uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from, sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
uint32_t samples) { uint32_t samples_count);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_write(&buf->buf, from, bytes);
}
static inline void static inline uint32_t
sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from, sc_audiobuf_capacity(struct sc_audiobuf *buf) {
uint32_t samples) { assert(buf->alloc_size);
size_t bytes = sc_audiobuf_to_bytes(buf, samples); return buf->alloc_size - 1;
sc_bytebuf_prepare_write(&buf->buf, from, bytes);
}
static inline void
sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_commit_write(&buf->buf, bytes);
} }
static inline uint32_t static inline uint32_t
sc_audiobuf_can_read(struct sc_audiobuf *buf) { sc_audiobuf_can_read(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_read(&buf->buf); uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
return sc_audiobuf_to_samples(buf, bytes); uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
} return (buf->alloc_size + head - tail) % buf->alloc_size;
static inline uint32_t
sc_audiobuf_can_write(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_write(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline uint32_t
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_capacity(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
sc_bytebuf_destroy(&buf->buf);
} }
#endif #endif

View File

@ -1,104 +0,0 @@
#include "bytebuf.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "util/log.h"
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
assert(alloc_size);
buf->data = malloc(alloc_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->alloc_size = alloc_size;
buf->head = 0;
buf->tail = 0;
return true;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf) {
free(buf->data);
}
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
size_t right_len = right_limit - buf->tail;
if (len < right_len) {
right_len = len;
}
memcpy(to, buf->data + buf->tail, right_len);
if (len > right_len) {
memcpy(to + right_len, buf->data, len - right_len);
}
buf->tail = (buf->tail + len) % buf->alloc_size;
}
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
buf->tail = (buf->tail + len) % buf->alloc_size;
}
static inline void
sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
size_t right_len = buf->alloc_size - buf->head;
if (len < right_len) {
right_len = len;
}
memcpy(buf->data + buf->head, from, right_len);
if (len > right_len) {
memcpy(buf->data, from + right_len, len - right_len);
}
}
static inline void
sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
buf->head = (buf->head + len) % buf->alloc_size;
}
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_write(buf));
sc_bytebuf_write_step0(buf, from, len);
sc_bytebuf_write_step1(buf, len);
}
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
// *This function MUST NOT access buf->tail (even in assert()).*
// The purpose of this function is to allow a reader and a writer to access
// different parts of the buffer in parallel simultaneously. It is intended
// to be called without lock (only sc_bytebuf_commit_write() is intended to
// be called with lock held).
assert(len < buf->alloc_size - 1);
sc_bytebuf_write_step0(buf, from, len);
}
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
assert(len <= sc_bytebuf_can_write(buf));
sc_bytebuf_write_step1(buf, len);
}

View File

@ -1,114 +0,0 @@
#ifndef SC_BYTEBUF_H
#define SC_BYTEBUF_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
struct sc_bytebuf {
uint8_t *data;
// The actual capacity is (allocated - 1) so that head == tail is
// non-ambiguous
size_t alloc_size;
size_t head; // writter cursor
size_t tail; // reader cursor
// empty: tail == head
// full: ((tail + 1) % alloc_size) == head
};
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
/**
* Copy from the bytebuf to a user-provided array
*
* The caller must check that len <= sc_bytebuf_read_available() (it is an
* error to attempt to read more bytes than available).
*
* This function is guaranteed not to write to buf->head.
*/
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
/**
* Drop len bytes from the buffer
*
* The caller must check that len <= sc_bytebuf_read_available() (it is an
* error to attempt to skip more bytes than available).
*
* This function is guaranteed not to write to buf->head.
*
* It is equivalent to call sc_bytebuf_read() to some array and discard the
* array (but this function is more efficient since there is no copy).
*/
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
/**
* Copy the user-provided array to the bytebuf
*
* The caller must check that len <= sc_bytebuf_write_available() (it is an
* error to write more bytes than the remaining available space).
*
* This function is guaranteed not to write to buf->tail.
*/
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
/**
* Copy the user-provided array to the bytebuf, but do not advance the cursor
*
* The caller must check that len <= sc_bytebuf_write_available() (it is an
* error to write more bytes than the remaining available space).
*
* After this function is called, the write must be committed with
* sc_bytebuf_commit_write().
*
* The purpose of this mechanism is to acquire a lock only to commit the write,
* but not to perform the actual copy.
*
* This function is guaranteed not to access buf->tail.
*/
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len);
/**
* Commit a prepared write
*/
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
/**
* Return the number of bytes which can be read
*
* It is an error to read more bytes than available.
*/
static inline size_t
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
}
/**
* Return the number of bytes which can be written
*
* It is an error to write more bytes than available.
*/
static inline size_t
sc_bytebuf_can_write(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
}
/**
* Return the actual capacity of the buffer (can_read() + can_write())
*/
static inline size_t
sc_bytebuf_capacity(struct sc_bytebuf *buf) {
return buf->alloc_size - 1;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf);
#endif

128
app/tests/test_audiobuf.c Normal file
View File

@ -0,0 +1,128 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/audiobuf.h"
static void test_audiobuf_simple(void) {
struct sc_audiobuf buf;
uint32_t data[20];
bool ok = sc_audiobuf_init(&buf, 4, 20);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5};
uint32_t w = sc_audiobuf_write(&buf, samples, 5);
assert(w == 5);
uint32_t r = sc_audiobuf_read(&buf, data, 4);
assert(r == 4);
assert(!memcmp(data, samples, 16));
uint32_t samples2[] = {6, 7, 8};
w = sc_audiobuf_write(&buf, samples2, 3);
assert(w == 3);
uint32_t single = 9;
w = sc_audiobuf_write(&buf, &single, 1);
assert(w == 1);
r = sc_audiobuf_read(&buf, &data[4], 8);
assert(r == 5);
uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
assert(!memcmp(data, expected, 36));
sc_audiobuf_destroy(&buf);
}
static void test_audiobuf_boundaries(void) {
struct sc_audiobuf buf;
uint32_t data[20];
bool ok = sc_audiobuf_init(&buf, 4, 20);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5, 6};
uint32_t w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
uint32_t r = sc_audiobuf_read(&buf, data, 9);
assert(r == 9);
uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3};
assert(!memcmp(data, expected, 36));
uint32_t samples2[] = {7, 8, 9, 10, 11};
w = sc_audiobuf_write(&buf, samples2, 5);
assert(w == 5);
uint32_t single = 12;
w = sc_audiobuf_write(&buf, &single, 1);
assert(w == 1);
w = sc_audiobuf_read(&buf, NULL, 3);
assert(w == 3);
assert(sc_audiobuf_can_read(&buf) == 12);
r = sc_audiobuf_read(&buf, data, 12);
assert(r == 12);
uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
assert(!memcmp(data, expected2, 48));
sc_audiobuf_destroy(&buf);
}
static void test_audiobuf_partial_read_write(void) {
struct sc_audiobuf buf;
uint32_t data[15];
bool ok = sc_audiobuf_init(&buf, 4, 10);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5, 6};
uint32_t w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 4);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 0);
uint32_t r = sc_audiobuf_read(&buf, data, 3);
assert(r == 3);
uint32_t expected[] = {1, 2, 3};
assert(!memcmp(data, expected, 12));
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 3);
r = sc_audiobuf_read(&buf, data, 15);
assert(r == 10);
uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3};
assert(!memcmp(data, expected2, 12));
sc_audiobuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_audiobuf_simple();
test_audiobuf_boundaries();
test_audiobuf_partial_read_write();
return 0;
}

View File

@ -1,126 +0,0 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/bytebuf.h"
static void test_bytebuf_simple(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
assert(sc_bytebuf_can_read(&buf) == 5);
sc_bytebuf_read(&buf, data, 4);
assert(!strncmp((char *) data, "hell", 4));
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
assert(sc_bytebuf_can_read(&buf) == 7);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 8);
sc_bytebuf_read(&buf, &data[4], 8);
assert(sc_bytebuf_can_read(&buf) == 0);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
static void test_bytebuf_boundaries(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 18);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 9));
assert(sc_bytebuf_can_read(&buf) == 9);
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
static void test_bytebuf_two_steps_write(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 3));
assert(sc_bytebuf_can_read(&buf) == 3);
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 9);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_bytebuf_simple();
test_bytebuf_boundaries();
test_bytebuf_two_steps_write();
return 0;
}

View File

@ -1,6 +1,7 @@
#include "common.h" #include "common.h"
#include <assert.h> #include <assert.h>
#include <stdint.h>
#include <string.h> #include <string.h>
#include "control_msg.h" #include "control_msg.h"
@ -16,11 +17,11 @@ static void test_serialize_inject_keycode(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 14); assert(size == 14);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
0x01, // AKEY_EVENT_ACTION_UP 0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
@ -38,11 +39,11 @@ static void test_serialize_inject_text(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 18); assert(size == 18);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TEXT, SC_CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x00, 0x00, 0x0d, // text length 0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
@ -58,11 +59,11 @@ static void test_serialize_inject_text_long(void) {
text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text; msg.inject_text.text = text;
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
unsigned char expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; uint8_t expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x00; expected[1] = 0x00;
expected[2] = 0x00; expected[2] = 0x00;
@ -95,11 +96,11 @@ static void test_serialize_inject_touch_event(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 32); assert(size == 32);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
0x00, // AKEY_EVENT_ACTION_DOWN 0x00, // AKEY_EVENT_ACTION_DOWN
0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id
@ -132,11 +133,11 @@ static void test_serialize_inject_scroll_event(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 21); assert(size == 21);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
@ -155,11 +156,11 @@ static void test_serialize_back_or_screen_on(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
0x01, // AKEY_EVENT_ACTION_UP 0x01, // AKEY_EVENT_ACTION_UP
}; };
@ -171,11 +172,11 @@ static void test_serialize_expand_notification_panel(void) {
.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -186,11 +187,11 @@ static void test_serialize_expand_settings_panel(void) {
.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -201,11 +202,11 @@ static void test_serialize_collapse_panels(void) {
.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -219,11 +220,11 @@ static void test_serialize_get_clipboard(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_COPY_KEY_COPY, SC_COPY_KEY_COPY,
}; };
@ -240,11 +241,11 @@ static void test_serialize_set_clipboard(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 27); assert(size == 27);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
1, // paste 1, // paste
@ -269,11 +270,11 @@ static void test_serialize_set_clipboard_long(void) {
text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0';
msg.set_clipboard.text = text; msg.set_clipboard.text = text;
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == SC_CONTROL_MSG_MAX_SIZE); assert(size == SC_CONTROL_MSG_MAX_SIZE);
unsigned char expected[SC_CONTROL_MSG_MAX_SIZE] = { uint8_t expected[SC_CONTROL_MSG_MAX_SIZE] = {
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
1, // paste 1, // paste
@ -296,11 +297,11 @@ static void test_serialize_set_screen_power_mode(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x02, // SC_SCREEN_POWER_MODE_NORMAL 0x02, // SC_SCREEN_POWER_MODE_NORMAL
}; };
@ -312,11 +313,11 @@ static void test_serialize_rotate_device(void) {
.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));

View File

@ -1,32 +1,32 @@
#include "common.h" #include "common.h"
#include <assert.h> #include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h> #include <string.h>
#include "device_msg.h" #include "device_msg.h"
#include <stdio.h>
static void test_deserialize_clipboard(void) { static void test_deserialize_clipboard(void) {
const unsigned char input[] = { const uint8_t input[] = {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,
0x00, 0x00, 0x00, 0x03, // text length 0x00, 0x00, 0x00, 0x03, // text length
0x41, 0x42, 0x43, // "ABC" 0x41, 0x42, 0x43, // "ABC"
}; };
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 8); assert(r == 8);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text); assert(msg.clipboard.text);
assert(!strcmp("ABC", msg.clipboard.text)); assert(!strcmp("ABC", msg.clipboard.text));
device_msg_destroy(&msg); sc_device_msg_destroy(&msg);
} }
static void test_deserialize_clipboard_big(void) { static void test_deserialize_clipboard_big(void) {
unsigned char input[DEVICE_MSG_MAX_SIZE]; uint8_t input[DEVICE_MSG_MAX_SIZE];
input[0] = DEVICE_MSG_TYPE_CLIPBOARD; input[0] = DEVICE_MSG_TYPE_CLIPBOARD;
input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24;
input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16;
@ -35,8 +35,8 @@ static void test_deserialize_clipboard_big(void) {
memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH);
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == DEVICE_MSG_MAX_SIZE); assert(r == DEVICE_MSG_MAX_SIZE);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
@ -44,17 +44,17 @@ static void test_deserialize_clipboard_big(void) {
assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH);
assert(msg.clipboard.text[0] == 'a'); assert(msg.clipboard.text[0] == 'a');
device_msg_destroy(&msg); sc_device_msg_destroy(&msg);
} }
static void test_deserialize_ack_set_clipboard(void) { static void test_deserialize_ack_set_clipboard(void) {
const unsigned char input[] = { const uint8_t input[] = {
DEVICE_MSG_TYPE_ACK_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
}; };
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 9); assert(r == 9);
assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD);

View File

@ -85,7 +85,7 @@ way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
To disable automatic clipboard synchronization, use To disable automatic clipboard synchronization, use
`--no-clipboard-autosync`. `--no-clipboard-autosync`.
## Pinch-to-zoom ## Pinch-to-zoom, rotate and tilt simulation
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_. To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
@ -93,8 +93,12 @@ More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
Until the left-click button is released, all mouse movements scale and rotate Until the left-click button is released, all mouse movements scale and rotate
the content (if supported by the app) relative to the center of the screen. the content (if supported by the app) relative to the center of the screen.
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
Technically, _scrcpy_ generates additional touch events from a "virtual finger" Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen. at a location inverted through the center of the screen. When pressing
<kbd>Ctrl</kbd> the x and y coordinates are inverted. Using <kbd>Shift</kbd>
only inverts x.
## Key repeat ## Key repeat

View File

@ -49,7 +49,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd> | Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> | Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd> | Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_ | Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer | Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)

View File

@ -16,5 +16,3 @@ endif
if get_option('compile_server') if get_option('compile_server')
subdir('server') subdir('server')
endif endif
run_target('run', command: ['scripts/run-scrcpy.sh'])

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"

View File

@ -153,13 +153,14 @@ public final class AudioCapture {
previousRecorderTimestamp = timestamp.nanoTime; previousRecorderTimestamp = timestamp.nanoTime;
} else { } else {
if (nextPts == 0) { if (nextPts == 0) {
Ln.w("Could not get any audio timestamp"); Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
} }
// compute from previous timestamp and packet size // compute from previous timestamp and packet size
pts = nextPts; pts = nextPts;
} }
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs; nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {

View File

@ -1,11 +1,8 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
/** /**
* Handle the cleanup of scrcpy, even if the main process is killed. * Handle the cleanup of scrcpy, even if the main process is killed.
@ -14,127 +11,59 @@ import java.io.IOException;
*/ */
public final class CleanUp { public final class CleanUp {
// A simple struct to be passed from the main process to the cleanup process private static final int MSG_TYPE_MASK = 0b11;
public static class Config implements Parcelable { private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
public static final Creator<Config> CREATOR = new Creator<Config>() { private static final int MSG_PARAM_SHIFT = 2;
@Override
public Config createFromParcel(Parcel in) {
return new Config(in);
}
@Override private final OutputStream out;
public Config[] newArray(int size) {
return new Config[size];
}
};
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; public CleanUp(OutputStream out) {
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; this.out = out;
private static final int FLAG_POWER_OFF_SCREEN = 4;
private int displayId;
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
private int restoreStayOn = -1;
private boolean disableShowTouches;
private boolean restoreNormalPowerMode;
private boolean powerOffScreen;
public Config() {
// Default constructor, the fields are initialized by CleanUp.configure()
}
protected Config(Parcel in) {
displayId = in.readInt();
restoreStayOn = in.readInt();
byte options = in.readByte();
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(displayId);
dest.writeInt(restoreStayOn);
byte options = 0;
if (disableShowTouches) {
options |= FLAG_DISABLE_SHOW_TOUCHES;
}
if (restoreNormalPowerMode) {
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
}
if (powerOffScreen) {
options |= FLAG_POWER_OFF_SCREEN;
}
dest.writeByte(options);
}
private boolean hasWork() {
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
}
@Override
public int describeContents() {
return 0;
}
byte[] serialize() {
Parcel parcel = Parcel.obtain();
writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();
return bytes;
}
static Config deserialize(byte[] bytes) {
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
return CREATOR.createFromParcel(parcel);
}
static Config fromBase64(String base64) {
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
return deserialize(bytes);
}
String toBase64() {
byte[] bytes = serialize();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
} }
private CleanUp() { public static CleanUp configure(int displayId) throws IOException {
// not instantiable String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
}
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
throws IOException {
Config config = new Config();
config.displayId = displayId;
config.disableShowTouches = disableShowTouches;
config.restoreStayOn = restoreStayOn;
config.restoreNormalPowerMode = restoreNormalPowerMode;
config.powerOffScreen = powerOffScreen;
if (config.hasWork()) {
startProcess(config);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
}
private static void startProcess(Config config) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.environment().put("CLASSPATH", Server.SERVER_PATH);
builder.start(); Process process = builder.start();
return new CleanUp(process.getOutputStream());
}
private boolean sendMessage(int type, int param) {
assert (type & ~MSG_TYPE_MASK) == 0;
int msg = type | param << MSG_PARAM_SHIFT;
try {
out.write(msg);
out.flush();
return true;
} catch (IOException e) {
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
return false;
}
}
public boolean setRestoreStayOn(int restoreValue) {
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
assert restoreValue >= -1 && restoreValue <= 7;
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
}
public boolean setDisableShowTouches(boolean disableOnExit) {
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
}
public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
}
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
} }
public static void unlinkSelf() { public static void unlinkSelf() {
@ -148,44 +77,71 @@ public final class CleanUp {
public static void main(String... args) { public static void main(String... args) {
unlinkSelf(); unlinkSelf();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = -1;
boolean disableShowTouches = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
try { try {
// Wait for the server to die // Wait for the server to die
System.in.read(); int msg;
while ((msg = System.in.read()) != -1) {
int type = msg & MSG_TYPE_MASK;
int param = msg >> MSG_PARAM_SHIFT;
switch (type) {
case MSG_TYPE_RESTORE_STAY_ON:
restoreStayOn = param > 7 ? -1 : param;
break;
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
disableShowTouches = param != 0;
break;
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
restoreNormalPowerMode = param != 0;
break;
case MSG_TYPE_POWER_OFF_SCREEN:
powerOffScreen = param != 0;
break;
default:
Ln.w("Unexpected msg type: " + type);
break;
}
}
} catch (IOException e) { } catch (IOException e) {
// Expected when the server is dead // Expected when the server is dead
} }
Ln.i("Cleaning up"); Ln.i("Cleaning up");
Config config = Config.fromBase64(args[0]); if (disableShowTouches) {
Ln.i("Disabling \"show touches\"");
if (config.disableShowTouches || config.restoreStayOn != -1) { try {
if (config.disableShowTouches) { Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
Ln.i("Disabling \"show touches\""); } catch (SettingsException e) {
try { Ln.e("Could not restore \"show_touches\"", e);
Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e);
}
} }
if (config.restoreStayOn != -1) { }
Ln.i("Restoring \"stay awake\"");
try { if (restoreStayOn != -1) {
Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); Ln.i("Restoring \"stay awake\"");
} catch (SettingsException e) { try {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
} } catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
} }
} }
if (Device.isScreenOn()) { if (Device.isScreenOn()) {
if (config.powerOffScreen) { if (powerOffScreen) {
Ln.i("Power off screen"); Ln.i("Power off screen");
Device.powerOffScreen(config.displayId); Device.powerOffScreen(displayId);
} else if (config.restoreNormalPowerMode) { } else if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode"); Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }
System.exit(0);
} }
} }

View File

@ -0,0 +1,33 @@
package com.genymobile.scrcpy;
import android.net.LocalSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public final class ControlChannel {
private final InputStream inputStream;
private final OutputStream outputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
public ControlChannel(LocalSocket controlSocket) throws IOException {
this.inputStream = controlSocket.getInputStream();
this.outputStream = controlSocket.getOutputStream();
}
public ControlMessage recv() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(inputStream);
msg = reader.next();
}
return msg;
}
public void send(DeviceMessage msg) throws IOException {
writer.writeTo(msg, outputStream);
}
}

View File

@ -27,7 +27,8 @@ public class Controller implements AsyncProcessor {
private Thread thread; private Thread thread;
private final Device device; private final Device device;
private final DesktopConnection connection; private final ControlChannel controlChannel;
private final CleanUp cleanUp;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
private final boolean clipboardAutosync; private final boolean clipboardAutosync;
private final boolean powerOn; private final boolean powerOn;
@ -41,13 +42,14 @@ public class Controller implements AsyncProcessor {
private boolean keepPowerModeOff; private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync, boolean powerOn) { public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
this.device = device; this.device = device;
this.connection = connection; this.controlChannel = controlChannel;
this.cleanUp = cleanUp;
this.clipboardAutosync = clipboardAutosync; this.clipboardAutosync = clipboardAutosync;
this.powerOn = powerOn; this.powerOn = powerOn;
initPointers(); initPointers();
sender = new DeviceMessageSender(connection); sender = new DeviceMessageSender(controlChannel);
} }
private void initPointers() { private void initPointers() {
@ -121,7 +123,7 @@ public class Controller implements AsyncProcessor {
} }
private void handleEvent() throws IOException { private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage(); ControlMessage msg = controlChannel.recv();
switch (msg.getType()) { switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
@ -170,6 +172,10 @@ public class Controller implements AsyncProcessor {
if (setPowerModeOk) { if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF; keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
if (cleanUp != null) {
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
}
} }
} }
break; break;

View File

@ -7,8 +7,6 @@ import android.net.LocalSocketAddress;
import java.io.Closeable; import java.io.Closeable;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public final class DesktopConnection implements Closeable { public final class DesktopConnection implements Closeable {
@ -24,25 +22,16 @@ public final class DesktopConnection implements Closeable {
private final FileDescriptor audioFd; private final FileDescriptor audioFd;
private final LocalSocket controlSocket; private final LocalSocket controlSocket;
private final InputStream controlInputStream; private final ControlChannel controlChannel;
private final OutputStream controlOutputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException { private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket; this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
this.audioSocket = audioSocket; this.audioSocket = audioSocket;
if (controlSocket != null) { this.controlSocket = controlSocket;
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
} else {
controlInputStream = null;
controlOutputStream = null;
}
videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null; videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
controlChannel = controlSocket != null ? new ControlChannel(controlSocket) : null;
} }
private static LocalSocket connect(String abstractName) throws IOException { private static LocalSocket connect(String abstractName) throws IOException {
@ -179,16 +168,7 @@ public final class DesktopConnection implements Closeable {
return audioFd; return audioFd;
} }
public ControlMessage receiveControlMessage() throws IOException { public ControlChannel getControlChannel() {
ControlMessage msg = reader.next(); return controlChannel;
while (msg == null) {
reader.readFrom(controlInputStream);
msg = reader.next();
}
return msg;
}
public void sendDeviceMessage(DeviceMessage msg) throws IOException {
writer.writeTo(msg, controlOutputStream);
} }
} }

View File

@ -45,11 +45,11 @@ public final class Device {
void onClipboardTextChanged(String text); void onClipboardTextChanged(String text);
} }
private final Size deviceSize;
private final Rect crop; private final Rect crop;
private int maxSize; private int maxSize;
private final int lockVideoOrientation; private final int lockVideoOrientation;
private Size deviceSize;
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private FoldListener foldListener; private FoldListener foldListener;
@ -116,8 +116,8 @@ public final class Device {
return; return;
} }
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(), deviceSize = displayInfo.getSize();
options.getMaxSize(), options.getLockVideoOrientation()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation);
// notify // notify
if (foldListener != null) { if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded); foldListener.onFoldChanged(displayId, folded);
@ -164,6 +164,10 @@ public final class Device {
} }
} }
public int getDisplayId() {
return displayId;
}
public synchronized void setMaxSize(int newMaxSize) { public synchronized void setMaxSize(int newMaxSize) {
maxSize = newMaxSize; maxSize = newMaxSize;
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);

View File

@ -4,7 +4,7 @@ import java.io.IOException;
public final class DeviceMessageSender { public final class DeviceMessageSender {
private final DesktopConnection connection; private final ControlChannel controlChannel;
private Thread thread; private Thread thread;
@ -12,8 +12,8 @@ public final class DeviceMessageSender {
private long ack; private long ack;
public DeviceMessageSender(DesktopConnection connection) { public DeviceMessageSender(ControlChannel controlChannel) {
this.connection = connection; this.controlChannel = controlChannel;
} }
public synchronized void pushClipboardText(String text) { public synchronized void pushClipboardText(String text) {
@ -43,11 +43,11 @@ public final class DeviceMessageSender {
if (sequence != DeviceMessage.SEQUENCE_INVALID) { if (sequence != DeviceMessage.SEQUENCE_INVALID) {
DeviceMessage event = DeviceMessage.createAckClipboard(sequence); DeviceMessage event = DeviceMessage.createAckClipboard(sequence);
connection.sendDeviceMessage(event); controlChannel.send(event);
} }
if (text != null) { if (text != null) {
DeviceMessage event = DeviceMessage.createClipboard(text); DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event); controlChannel.send(event);
} }
} }
} }

View File

@ -1,8 +1,10 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.display.VirtualDisplay;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
@ -11,6 +13,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
private final Device device; private final Device device;
private IBinder display; private IBinder display;
private VirtualDisplay virtualDisplay;
public ScreenCapture(Device device) { public ScreenCapture(Device device) {
this.device = device; this.device = device;
@ -34,9 +37,29 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
if (display != null) { if (display != null) {
SurfaceControl.destroyDisplay(display); SurfaceControl.destroyDisplay(display);
display = null;
}
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
try {
display = createDisplay();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
Ln.d("Display: using SurfaceControl API");
} catch (Exception surfaceControlException) {
Rect videoRect = screenInfo.getVideoSize().toRect();
try {
virtualDisplay = ServiceManager.getDisplayManager()
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
Ln.e("Could not create display using DisplayManager", displayManagerException);
throw new AssertionError("Could not create display");
}
} }
display = createDisplay();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
} }
@Override @Override
@ -69,7 +92,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList
requestReset(); requestReset();
} }
private static IBinder createDisplay() { private static IBinder createDisplay() throws Exception {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals( boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(

View File

@ -51,46 +51,47 @@ public final class Server {
// not instantiable // not instantiable
} }
private static void initAndCleanUp(Options options) { private static void initAndCleanUp(Options options, CleanUp cleanUp) {
boolean mustDisableShowTouchesOnCleanUp = false; // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
int restoreStayOn = -1; // and for all, they cannot be changed from another thread)
boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled
if (options.getShowTouches() || options.getStayAwake()) {
if (options.getShowTouches()) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) { if (options.getShowTouches()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; try {
try { String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); // If "show touches" was disabled, it must be disabled back on clean up
try { if (!"1".equals(oldValue)) {
restoreStayOn = Integer.parseInt(oldValue); if (!cleanUp.setDisableShowTouches(true)) {
if (restoreStayOn == stayOn) { Ln.e("Could not disable show touch on exit");
// No need to restore
restoreStayOn = -1;
}
} catch (NumberFormatException e) {
restoreStayOn = 0;
} }
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
} }
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
} }
} }
if (options.getCleanup()) { if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try { try {
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
options.getPowerOffScreenOnClose()); try {
} catch (IOException e) { int restoreStayOn = Integer.parseInt(oldValue);
Ln.e("Could not configure cleanup", e); if (restoreStayOn != stayOn) {
// Restore only if the current value is different
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
Ln.e("Could not restore stay on on exit");
}
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
if (options.getPowerOffScreenOnClose()) {
if (!cleanUp.setPowerOffScreen(true)) {
Ln.e("Could not power off screen on exit");
} }
} }
} }
@ -101,7 +102,13 @@ public final class Server {
throw new ConfigurationException("Camera mirroring is not supported"); throw new ConfigurationException("Camera mirroring is not supported");
} }
Thread initThread = startInitThread(options); CleanUp cleanUp = null;
Thread initThread = null;
if (options.getCleanup()) {
cleanUp = CleanUp.configure(options.getDisplayId());
initThread = startInitThread(options, cleanUp);
}
int scid = options.getScid(); int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
@ -124,7 +131,8 @@ public final class Server {
} }
if (control) { if (control) {
Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); ControlChannel controlChannel = connection.getControlChannel();
Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text)); device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
asyncProcessors.add(controller); asyncProcessors.add(controller);
} }
@ -167,7 +175,9 @@ public final class Server {
completion.await(); completion.await();
} finally { } finally {
initThread.interrupt(); if (initThread != null) {
initThread.interrupt();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop(); asyncProcessor.stop();
} }
@ -175,7 +185,9 @@ public final class Server {
connection.shutdown(); connection.shutdown();
try { try {
initThread.join(); if (initThread != null) {
initThread.join();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join(); asyncProcessor.join();
} }
@ -187,8 +199,8 @@ public final class Server {
} }
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup"); Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
thread.start(); thread.start();
return thread; return thread;
} }

View File

@ -285,16 +285,28 @@ public final class Workarounds {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
// private native int native_setup(Object audiorecordThis, if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Object /*AudioAttributes*/ attributes, // private native int native_setup(Object audiorecordThis,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, // Object /*AudioAttributes*/ attributes,
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
nativeSetupMethod.setAccessible(true); int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray, nativeSetupMethod.setAccessible(true);
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0); initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
attributionSourceParcel, 0L, 0);
} else {
// Android 14 added a new int parameter "halInputFlags"
// <https://github.com/aosp-mirror/platform_frameworks_base/commit/f6135d75db79b1d48fad3a3b3080d37be20a2313>
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class,
int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class);
nativeSetupMethod.setAccessible(true);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes,
sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session,
attributionSourceParcel, 0L, 0, 0);
}
} }
} }

View File

@ -13,7 +13,6 @@ import android.os.IBinder;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
@ -26,7 +25,20 @@ public final class ActivityManager {
private Method startActivityAsUserWithFeatureMethod; private Method startActivityAsUserWithFeatureMethod;
private Method forceStopPackageMethod; private Method forceStopPackageMethod;
public ActivityManager(IInterface manager) { static ActivityManager create() {
try {
// On old Android versions, the ActivityManager is not exposed via AIDL,
// so use ActivityManagerNative.getDefault()
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
IInterface am = (IInterface) getDefaultMethod.invoke(null);
return new ActivityManager(am);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private ActivityManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
@ -76,7 +88,7 @@ public final class ActivityManager {
return null; return null;
} }
return new ContentProvider(this, provider, name, token); return new ContentProvider(this, provider, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }
@ -86,7 +98,7 @@ public final class ActivityManager {
try { try {
Method method = getRemoveContentProviderExternalMethod(); Method method = getRemoveContentProviderExternalMethod();
method.invoke(manager, name, token); method.invoke(manager, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
} }
} }

View File

@ -8,7 +8,6 @@ import android.content.IOnPrimaryClipChangedListener;
import android.os.Build; import android.os.Build;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
public final class ClipboardManager { public final class ClipboardManager {
@ -20,7 +19,18 @@ public final class ClipboardManager {
private int setMethodVersion; private int setMethodVersion;
private int addListenerMethodVersion; private int addListenerMethodVersion;
public ClipboardManager(IInterface manager) { static ClipboardManager create() {
IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
if (clipboard == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
return new ClipboardManager(clipboard);
}
private ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
@ -41,8 +51,21 @@ public final class ClipboardManager {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2; getMethodVersion = 2;
} catch (NoSuchMethodException e3) { } catch (NoSuchMethodException e3) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); try {
getMethodVersion = 3; getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
} catch (NoSuchMethodException e4) {
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
} catch (NoSuchMethodException e5) {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class,
boolean.class);
getMethodVersion = 5;
}
}
} }
} }
} }
@ -64,9 +87,15 @@ public final class ClipboardManager {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1; setMethodVersion = 1;
} catch (NoSuchMethodException e2) { } catch (NoSuchMethodException e2) {
setPrimaryClipMethod = manager.getClass() try {
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); setPrimaryClipMethod = manager.getClass()
setMethodVersion = 2; .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
} catch (NoSuchMethodException e3) {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
} }
} }
} }
@ -74,8 +103,7 @@ public final class ClipboardManager {
return setPrimaryClipMethod; return setPrimaryClipMethod;
} }
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
} }
@ -87,13 +115,17 @@ public final class ClipboardManager {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2: case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
default: case 3:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
case 4:
// The last boolean parameter is "userOperate"
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
} }
} }
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return; return;
@ -106,9 +138,12 @@ public final class ClipboardManager {
case 1: case 1:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break; break;
default: case 2:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break; break;
default:
// The last boolean parameter is "userOperate"
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
} }
} }
@ -120,7 +155,7 @@ public final class ClipboardManager {
return null; return null;
} }
return clipData.getItemAt(0).getText(); return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }
@ -132,14 +167,14 @@ public final class ClipboardManager {
ClipData clipData = ClipData.newPlainText(null, text); ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, setMethodVersion, manager, clipData); setPrimaryClip(method, setMethodVersion, manager, clipData);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }
} }
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
throws InvocationTargetException, IllegalAccessException { throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME); method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return; return;
@ -191,7 +226,7 @@ public final class ClipboardManager {
Method method = getAddPrimaryClipChangedListener(); Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }

View File

@ -11,7 +11,6 @@ import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import java.io.Closeable; import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
public final class ContentProvider implements Closeable { public final class ContentProvider implements Closeable {
@ -42,8 +41,6 @@ public final class ContentProvider implements Closeable {
private Method callMethod; private Method callMethod;
private int callMethodVersion; private int callMethodVersion;
private Object attributionSource;
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
this.manager = manager; this.manager = manager;
this.provider = provider; this.provider = provider;
@ -77,8 +74,7 @@ public final class ContentProvider implements Closeable {
return callMethod; return callMethod;
} }
private Bundle call(String callMethod, String arg, Bundle extras) private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException {
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
try { try {
Method method = getCallMethod(); Method method = getCallMethod();
Object[] args; Object[] args;
@ -99,7 +95,7 @@ public final class ContentProvider implements Closeable {
} }
} }
return (Bundle) method.invoke(provider, args); return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
throw e; throw e;
} }

View File

@ -7,7 +7,6 @@ import android.annotation.TargetApi;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) @SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"})
@ -55,7 +54,7 @@ public final class DisplayControl {
try { try {
Method method = getGetPhysicalDisplayTokenMethod(); Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId); return (IBinder) method.invoke(null, physicalDisplayId);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }
@ -72,7 +71,7 @@ public final class DisplayControl {
try { try {
Method method = getGetPhysicalDisplayIdsMethod(); Method method = getGetPhysicalDisplayIdsMethod();
return (long[]) method.invoke(null); return (long[]) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }

View File

@ -5,16 +5,33 @@ import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.Size; import com.genymobile.scrcpy.Size;
import android.annotation.SuppressLint;
import android.hardware.display.VirtualDisplay;
import android.view.Display; import android.view.Display;
import android.view.Surface;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class DisplayManager { public final class DisplayManager {
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method createVirtualDisplayMethod;
public DisplayManager(Object manager) { static DisplayManager create() {
try {
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
Object dmg = getInstanceMethod.invoke(null);
return new DisplayManager(dmg);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private DisplayManager(Object manager) {
this.manager = manager; this.manager = manager;
} }
@ -60,7 +77,7 @@ public final class DisplayManager {
try { try {
Field filed = Display.class.getDeclaredField(flagString); Field filed = Display.class.getDeclaredField(flagString);
flags |= filed.getInt(null); flags |= filed.getInt(null);
} catch (NoSuchFieldException | IllegalAccessException e) { } catch (ReflectiveOperationException e) {
// Silently ignore, some flags reported by "dumpsys display" are @TestApi // Silently ignore, some flags reported by "dumpsys display" are @TestApi
} }
} }
@ -82,7 +99,7 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
} catch (Exception e) { } catch (ReflectiveOperationException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
@ -90,8 +107,21 @@ public final class DisplayManager {
public int[] getDisplayIds() { public int[] getDisplayIds() {
try { try {
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
} catch (Exception e) { } catch (ReflectiveOperationException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException {
if (createVirtualDisplayMethod == null) {
createVirtualDisplayMethod = android.hardware.display.DisplayManager.class
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class);
}
return createVirtualDisplayMethod;
}
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception {
Method method = getCreateVirtualDisplayMethod();
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
}
} }

View File

@ -2,12 +2,13 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class InputManager { public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
@ -20,7 +21,27 @@ public final class InputManager {
private static Method setDisplayIdMethod; private static Method setDisplayIdMethod;
private static Method setActionButtonMethod; private static Method setActionButtonMethod;
public InputManager(Object manager) { static InputManager create() {
try {
Class<?> inputManagerClass = getInputManagerClass();
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
return new InputManager(im);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private static Class<?> getInputManagerClass() {
try {
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
return Class.forName("android.hardware.input.InputManagerGlobal");
} catch (ClassNotFoundException e) {
return android.hardware.input.InputManager.class;
}
}
private InputManager(Object manager) {
this.manager = manager; this.manager = manager;
} }
@ -35,7 +56,7 @@ public final class InputManager {
try { try {
Method method = getInjectInputEventMethod(); Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode); return (boolean) method.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }
@ -53,7 +74,7 @@ public final class InputManager {
Method method = getSetDisplayIdMethod(); Method method = getSetDisplayIdMethod();
method.invoke(inputEvent, displayId); method.invoke(inputEvent, displayId);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Cannot associate a display id to the input event", e); Ln.e("Cannot associate a display id to the input event", e);
return false; return false;
} }
@ -71,7 +92,7 @@ public final class InputManager {
Method method = getSetActionButtonMethod(); Method method = getSetActionButtonMethod();
method.invoke(motionEvent, actionButton); method.invoke(motionEvent, actionButton);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Cannot set action button on MotionEvent", e); Ln.e("Cannot set action button on MotionEvent", e);
return false; return false;
} }

View File

@ -6,14 +6,18 @@ import android.annotation.SuppressLint;
import android.os.Build; import android.os.Build;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
public final class PowerManager { public final class PowerManager {
private final IInterface manager; private final IInterface manager;
private Method isScreenOnMethod; private Method isScreenOnMethod;
public PowerManager(IInterface manager) { static PowerManager create() {
IInterface manager = ServiceManager.getService("power", "android.os.IPowerManager");
return new PowerManager(manager);
}
private PowerManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
@ -30,7 +34,7 @@ public final class PowerManager {
try { try {
Method method = getIsScreenOnMethod(); Method method = getIsScreenOnMethod();
return (boolean) method.invoke(manager); return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }

View File

@ -9,7 +9,6 @@ import android.os.IBinder;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
@ -38,7 +37,7 @@ public final class ServiceManager {
/* not instantiable */ /* not instantiable */
} }
private static IInterface getService(String service, String type) { static IInterface getService(String service, String type) {
try { try {
IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service);
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
@ -50,90 +49,51 @@ public final class ServiceManager {
public static WindowManager getWindowManager() { public static WindowManager getWindowManager() {
if (windowManager == null) { if (windowManager == null) {
windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); windowManager = WindowManager.create();
} }
return windowManager; return windowManager;
} }
public static DisplayManager getDisplayManager() { public static DisplayManager getDisplayManager() {
if (displayManager == null) { if (displayManager == null) {
try { displayManager = DisplayManager.create();
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
Object dmg = getInstanceMethod.invoke(null);
displayManager = new DisplayManager(dmg);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
} }
return displayManager; return displayManager;
} }
public static Class<?> getInputManagerClass() {
try {
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
return Class.forName("android.hardware.input.InputManagerGlobal");
} catch (ClassNotFoundException e) {
return android.hardware.input.InputManager.class;
}
}
public static InputManager getInputManager() { public static InputManager getInputManager() {
if (inputManager == null) { if (inputManager == null) {
try { inputManager = InputManager.create();
Class<?> inputManagerClass = getInputManagerClass();
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
inputManager = new InputManager(im);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
} }
return inputManager; return inputManager;
} }
public static PowerManager getPowerManager() { public static PowerManager getPowerManager() {
if (powerManager == null) { if (powerManager == null) {
powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); powerManager = PowerManager.create();
} }
return powerManager; return powerManager;
} }
public static StatusBarManager getStatusBarManager() { public static StatusBarManager getStatusBarManager() {
if (statusBarManager == null) { if (statusBarManager == null) {
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); statusBarManager = StatusBarManager.create();
} }
return statusBarManager; return statusBarManager;
} }
public static ClipboardManager getClipboardManager() { public static ClipboardManager getClipboardManager() {
if (clipboardManager == null) { if (clipboardManager == null) {
IInterface clipboard = getService("clipboard", "android.content.IClipboard"); // May be null, some devices have no clipboard manager
if (clipboard == null) { clipboardManager = ClipboardManager.create();
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
clipboardManager = new ClipboardManager(clipboard);
} }
return clipboardManager; return clipboardManager;
} }
public static ActivityManager getActivityManager() { public static ActivityManager getActivityManager() {
if (activityManager == null) { if (activityManager == null) {
try { activityManager = ActivityManager.create();
// On old Android versions, the ActivityManager is not exposed via AIDL,
// so use ActivityManagerNative.getDefault()
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
IInterface am = (IInterface) getDefaultMethod.invoke(null);
activityManager = new ActivityManager(am);
} catch (Exception e) {
throw new AssertionError(e);
}
} }
return activityManager; return activityManager;
} }

View File

@ -4,7 +4,6 @@ import com.genymobile.scrcpy.Ln;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
public final class StatusBarManager { public final class StatusBarManager {
@ -16,7 +15,12 @@ public final class StatusBarManager {
private boolean expandSettingsPanelMethodNewVersion = true; private boolean expandSettingsPanelMethodNewVersion = true;
private Method collapsePanelsMethod; private Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) { static StatusBarManager create() {
IInterface manager = ServiceManager.getService("statusbar", "com.android.internal.statusbar.IStatusBarService");
return new StatusBarManager(manager);
}
private StatusBarManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
@ -62,7 +66,7 @@ public final class StatusBarManager {
} else { } else {
method.invoke(manager); method.invoke(manager);
} }
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
} }
} }
@ -77,7 +81,7 @@ public final class StatusBarManager {
// old version // old version
method.invoke(manager); method.invoke(manager);
} }
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
} }
} }
@ -86,7 +90,7 @@ public final class StatusBarManager {
try { try {
Method method = getCollapsePanelsMethod(); Method method = getCollapsePanelsMethod();
method.invoke(manager); method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
} }
} }

View File

@ -8,7 +8,6 @@ import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
@ -78,12 +77,8 @@ public final class SurfaceControl {
} }
} }
public static IBinder createDisplay(String name, boolean secure) { public static IBinder createDisplay(String name, boolean secure) throws Exception {
try { return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
} catch (Exception e) {
throw new AssertionError(e);
}
} }
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
@ -109,7 +104,7 @@ public final class SurfaceControl {
// call getInternalDisplayToken() // call getInternalDisplayToken()
return (IBinder) method.invoke(null); return (IBinder) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }
@ -126,7 +121,7 @@ public final class SurfaceControl {
try { try {
Method method = getGetPhysicalDisplayTokenMethod(); Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId); return (IBinder) method.invoke(null, physicalDisplayId);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }
@ -152,7 +147,7 @@ public final class SurfaceControl {
try { try {
Method method = getGetPhysicalDisplayIdsMethod(); Method method = getGetPhysicalDisplayIdsMethod();
return (long[]) method.invoke(null); return (long[]) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }
@ -170,7 +165,7 @@ public final class SurfaceControl {
Method method = getSetDisplayPowerModeMethod(); Method method = getSetDisplayPowerModeMethod();
method.invoke(null, displayToken, mode); method.invoke(null, displayToken, mode);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }

View File

@ -7,7 +7,6 @@ import android.os.IInterface;
import android.view.IDisplayFoldListener; import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
public final class WindowManager { public final class WindowManager {
@ -17,7 +16,12 @@ public final class WindowManager {
private Method isRotationFrozenMethod; private Method isRotationFrozenMethod;
private Method thawRotationMethod; private Method thawRotationMethod;
public WindowManager(IInterface manager) { static WindowManager create() {
IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager");
return new WindowManager(manager);
}
private WindowManager(IInterface manager) {
this.manager = manager; this.manager = manager;
} }
@ -61,7 +65,7 @@ public final class WindowManager {
try { try {
Method method = getGetRotationMethod(); Method method = getGetRotationMethod();
return (int) method.invoke(manager); return (int) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return 0; return 0;
} }
@ -71,7 +75,7 @@ public final class WindowManager {
try { try {
Method method = getFreezeRotationMethod(); Method method = getFreezeRotationMethod();
method.invoke(manager, rotation); method.invoke(manager, rotation);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
} }
} }
@ -80,7 +84,7 @@ public final class WindowManager {
try { try {
Method method = getIsRotationFrozenMethod(); Method method = getIsRotationFrozenMethod();
return (boolean) method.invoke(manager); return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return false; return false;
} }
@ -90,7 +94,7 @@ public final class WindowManager {
try { try {
Method method = getThawRotationMethod(); Method method = getThawRotationMethod();
method.invoke(manager); method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
} }
} }