Compare commits

..

46 Commits

Author SHA1 Message Date
Romain Vimont
5369c4f754 Serialize clean-up configuration
This avoids to pass each option as individual parameter and parse them
manually (it's still "manual" in the Parcelable implementation).

Refs #824 <https://github.com/Genymobile/scrcpy/pull/824#issuecomment-780319422>

Reviewed-by: Yu-Chen Lin <npes87184@gmail.com>
2021-05-01 14:14:32 +02:00
Romain Vimont
233f8e6cc4 Rename keycode injection method
Make it explicit that it injects both "press" and "release" events.
2021-04-30 23:07:37 +02:00
Romain Vimont
9a7d351d67 Simplify non-static injectEvent() implementation
Just call the static version (having a displayId) from the non-static
version (using the displayId field).
2021-04-30 23:07:37 +02:00
Romain Vimont
d00ee640c0 Simplify Device.java
Remove useless intermediate method with a "mode" parameter (it's always
called with the same mode).

This also avoids the need for a specific injectEventOnDisplay() method,
since it does not conflict with another injectEvent() method anymore.
2021-04-30 23:07:29 +02:00
Romain Vimont
ae6ec7a194 Unref decoder AVFrame immediately
The frame can be unref immediately after it is pushed to the frame
sinks.

It was not really a memory leak because the frame was unref every time
by avcodec_receive_frame() (and freed on close), but a reference was
unnecessarily kept for too long.
2021-04-26 18:05:43 +02:00
Romain Vimont
84f17fdeab Fix v4l2 AVPacket memory leak on error
Unref v4l2 AVPacket even if writing failed.
2021-04-26 18:05:11 +02:00
Romain Vimont
1cde68a1fa Fix v4l2 AVFrame memory leak
Unref frame immediately once encoded.

Fixes #2279 <https://github.com/Genymobile/scrcpy/pull/2279>
2021-04-26 18:04:51 +02:00
Romain Vimont
45e7280148 Rename --v4l2_sink to --v4l2-sink
This was a rebase issue, the previous version in #2268 was correct.

Refs #2268 <https://github.com/Genymobile/scrcpy/pull/2268>
2021-04-26 17:59:35 +02:00
Romain Vimont
41a0383d7c Document v4l2 sink in README 2021-04-25 15:00:56 +02:00
Marco Martinelli
d39161f753 Add support for v4l2loopback
It allows to send the video stream to /dev/videoN, so that it can be
captured (like a webcam) by any v4l2-capable tool.

PR #2232 <https://github.com/Genymobile/scrcpy/pull/2232>
PR #2233 <https://github.com/Genymobile/scrcpy/pull/2233>
PR #2268 <https://github.com/Genymobile/scrcpy/pull/2268>

Co-authored-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:59:10 +02:00
Romain Vimont
5af9d0ee0f Make --lock-video-orientation argument optional
If the option is set without argument, lock the initial device
orientation (as if the value "initial" was passed).
2021-04-25 14:55:54 +02:00
Romain Vimont
fd0dc6c0cd Add --lock-video-orientation=initial
Add a new mode to the --lock-video-orientation option, to lock the
initial orientation of the device.

This avoids to pass an explicit value (0, 1, 2 or 3) and think about
which is the right one.
2021-04-25 14:55:54 +02:00
Romain Vimont
151bc16644 Use strlist_contains() to find a muxer
The AVOutputFormat name is a string list: it contains names separated by
',' (possibly only one).
2021-04-25 14:55:19 +02:00
Romain Vimont
ffc00210e9 Add strlist_contains()
Add a function to know if a string list, using some separator, contains
a specific string.
2021-04-25 14:38:42 +02:00
Romain Vimont
243854a408 Fix recorder comment 2021-04-25 14:38:42 +02:00
Romain Vimont
8b90dc61b9 Handle EAGAIN on send_packet in decoder
EAGAIN was only handled on receive_frame.

In practice, it should not be necessary, since one packet always
contains one frame. But just in case.
2021-04-25 14:38:42 +02:00
Romain Vimont
2a5dfc1c17 Handle errors using gotos in recorder_open()
There are many initializations in recorder_open(). Handle RAII-like
deinitialization using gotos.
2021-04-25 14:38:42 +02:00
Romain Vimont
e3fccc5a5e Initialize recorder fields on open
Only initialize ops and parameters copy from recorder_init(). It was
inconsistent to initialize some fields from _init() and some others from
_open().
2021-04-25 14:38:42 +02:00
Romain Vimont
0541f1bff2 Hide the window immediately on close
The screen may not be destroyed immediately on close to avoid undefined
behavior, because it may still receive events from the decoder.

But the visual window must still be closed immediately.
2021-04-25 14:38:42 +02:00
Romain Vimont
0272e6dc77 Assert screen closed on destroy
The destruction order is important, but tricky, because the screen is
open/close by the decoder, but destroyed by scrcpy.c on the main thread.

Add assertions to guarantee that the screen is not destroyed before
being closed.
2021-04-25 14:38:42 +02:00
Romain Vimont
2a94a2b119 Remove video_buffer callbacks
Now that screen is both the owner and the listener of the video buffer,
execute the code directly without callbacks.
2021-04-25 14:38:42 +02:00
Romain Vimont
e91acdb0c4 Move video_buffer to screen
The video buffer is now an internal detail of the screen component.

Since the screen is plugged to the decoder via the frame sink trait, the
decoder does not access to the video buffer anymore.
2021-04-25 14:38:42 +02:00
Romain Vimont
6f5ad21f57 Make decoder push frames to sinks
Now that screen implements the packet sink trait, make decoder push
packets to the sinks without depending on the concrete sink types.
2021-04-25 14:38:42 +02:00
Romain Vimont
08b3086ffc Expose screen as frame sink
Make screen implement the frame sink trait.

This will allow the decoder to push frames without depending on the
concrete sink type.
2021-04-25 14:38:42 +02:00
Romain Vimont
deab7da761 Add frame sink trait
This trait will allow to abstract the concrete sink types from the frame
producer (decoder.c).
2021-04-25 14:38:42 +02:00
Romain Vimont
f7a1b67d66 Make stream push packets to sinks
Now that decoder and recorder implement the packet sink trait, make
stream push packets to the sinks without depending on the concrete sink
types.
2021-04-25 14:38:42 +02:00
Romain Vimont
cbed38799e Expose decoder as packet sink
Make decoder implement the packet sink trait.

This will allow the stream to push packets without depending on the
concrete sink type.
2021-04-25 14:38:42 +02:00
Romain Vimont
5beb7d6c02 Reorder decoder functions
This will make further commits more readable.
2021-04-25 14:38:42 +02:00
Romain Vimont
5980183a33 Expose recorder as packet sink
Make recorder implement the packet sink trait.

This will allow the stream to push packets without depending on the
concrete sink type.
2021-04-25 14:38:42 +02:00
Romain Vimont
fe8de893ca Privatize recorder threading
The fact that the recorder uses a separate thread is an internal detail,
so the functions _start(), _stop() and _join() should not be exposed.

Instead, start the thread on _open() and _stop()+_join() on close().

This paves the way to expose the recorder as a packet sink trait.
2021-04-25 14:38:42 +02:00
Romain Vimont
a974483c15 Reorder recorder functions
This will make further commits more readable.
2021-04-25 14:38:42 +02:00
Romain Vimont
1b072a24c4 Add packet sink trait
This trait will allow to abstract the concrete sink types from the
packet producer (stream.c).
2021-04-25 14:38:42 +02:00
Romain Vimont
08f1fd46c8 Add container_of() macro
This will allow to get the parent of an embedded struct.
2021-04-25 14:38:42 +02:00
Romain Vimont
2ddf760c09 Make video_buffer more generic
The video buffer took ownership of the producer frame (so that it could
swap frames quickly).

In order to support multiple sinks plugged to the decoder, the decoded
frame must not be consumed by the display video buffer.

Therefore, move the producer and consumer frames out of the video
buffer, and use FFmpeg AVFrame refcounting to share ownership while
avoiding copies.
2021-04-25 14:38:42 +02:00
Romain Vimont
5d9e96dc4e Remove compat with old FFmpeg codec params API
The new API has been introduced in 2016 in libavformat 57.xx, it's very
old.

This will avoid to maintain two code paths for codec parameters.
2021-04-25 14:38:42 +02:00
Romain Vimont
de9b79ec2d Remove compat with old FFmpeg decoding API
The new API has been introduced in 2016 in libavcodec 57.xx, it's very
old.

This will avoid to maintain two code paths for decoding.
2021-04-25 14:38:42 +02:00
Romain Vimont
55806e7d31 Remove option --render-expired-frames
This flag forced the decoder to wait for the previous frame to be
consumed by the display.

It was initially implemented as a compilation flag for testing, not
intended to be exposed at runtime. But to remove ifdefs and to allow
users to test this flag easily, it had finally been exposed by commit
ebccb9f6cc.

In practice, it turned out to be useless: it had no practical impact,
and it did not solve or mitigate any performance issues causing frame
skipping.

But that added some complexity to the codebase: it required an
additional condition variable, and made video buffer calls possibly
blocking, which in turn required code to interrupt it on exit.

To prepare support for multiple sinks plugged to the decoder (display
and v4l2 for example), the blocking call used for pacing the decoder
output becomes unacceptable, so just remove this useless "feature".
2021-04-25 14:38:42 +02:00
Romain Vimont
21b590b766 Write trailer from recorder thread
The recorder thread wrote the whole content except the trailer, which
was odd.
2021-04-25 14:38:42 +02:00
brunoais
d7e6589677 Document 4th+5th + 2xn shortcuts
PR #2260 <https://github.com/Genymobile/scrcpy/pull/2260>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
brunoais
b4ee9f27ce Add mouse shortcut to expand settings panel
Double-click on extra mouse button to open the settings panel (a
single-click opens the notification panel).

This is consistent with the keyboard shortcut MOD+n+n.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
brunoais
6fa63cf6f8 Add keyboard shortcut to expand settings panel
PR #2260 <https://github.com/Genymobile/scrcpy/pull/2260>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
brunoais
50eecdab28 Add control message to expand settings panel
PR #2260 <https://github.com/Genymobile/scrcpy/pull/2260>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
brunoais
9576283907 Count repeated identical key events
This will allow shortcuts such as MOD+n+n to open the settings panel.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
brunoais
66c581851f Rename control message type to COLLAPSE_PANELS
The collapsing action collapses any panels.

By the way, the Android method is named collapsePanels().

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
Romain Vimont
bb4614d558 Reverse boolean logic for readability
Refs #2260 <https://github.com/Genymobile/scrcpy/pull/2260#issuecomment-823508759>
2021-04-25 14:36:48 +02:00
Frank Leon Rose
aaf7875d92 Ensure get_server_path() retval is freeable
PR #2276 <https://github.com/Genymobile/scrcpy/pull/2276>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-22 22:12:23 +02:00
18 changed files with 294 additions and 87 deletions

View File

@@ -301,7 +301,7 @@ vlc v4l2:///dev/videoN # VLC might add some buffering delay
For example, you could capture the video within [OBS].
[OBS]: https://obsproject.com/
[OBS]: https://obsproject.com/fr
### Connection
@@ -741,10 +741,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-click¹_
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd>
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
| Click on `MENU` (unlock screen) | <kbd>MOD</kbd>+<kbd>m</kbd>
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
@@ -753,18 +753,27 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd>
| Collapse notification panel | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard³ | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard³ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste³ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
| Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
| Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard⁴ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste⁴ | <kbd>MOD</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>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
Only on Android >= 7._
4th and 5th mouse buttons, if your mouse has them._
_⁴Only on Android >= 7._
Shortcuts with repeated keys are executted by releasing and pressing the key a
second time. For example, to execute "Expand settings panel":
1. Press and keep pressing <kbd>MOD</kbd>.
2. Then double-press <kbd>n</kbd>.
3. Finally, release <kbd>MOD</kbd>.
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
handled by the active application.

View File

@@ -81,7 +81,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[1] = msg->set_screen_power_mode.mode;
return 2;
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data

View File

@@ -27,7 +27,8 @@ enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,

View File

@@ -111,6 +111,8 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) {
// A frame lost should not make the whole pipeline fail. The error, if
// any, is already logged.
(void) ok;
av_frame_unref(decoder->frame);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
return false;

View File

@@ -72,6 +72,10 @@ input_manager_init(struct input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false;
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
}
static void
@@ -179,9 +183,19 @@ expand_notification_panel(struct controller *controller) {
}
static void
collapse_notification_panel(struct controller *controller) {
expand_settings_panel(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'expand settings panel'");
}
}
static void
collapse_panels(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'collapse notification panel'");
@@ -384,16 +398,27 @@ input_manager_process_key(struct input_manager *im,
// control: indicates the state of the command-line option --no-control
bool control = im->control;
bool smod = is_shortcut_mod(im, event->keysym.mod);
struct controller *controller = im->controller;
SDL_Keycode keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod;
bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat;
bool smod = is_shortcut_mod(im, mod);
if (down && !repeat) {
if (keycode == im->last_keycode && mod == im->last_mod) {
++im->key_repeat;
} else {
im->key_repeat = 0;
im->last_keycode = keycode;
im->last_mod = mod;
}
}
// The shortcut modifier is pressed
if (smod) {
int action = down ? ACTION_DOWN : ACTION_UP;
@@ -498,9 +523,11 @@ input_manager_process_key(struct input_manager *im,
case SDLK_n:
if (control && !repeat && down) {
if (shift) {
collapse_notification_panel(controller);
} else {
collapse_panels(controller);
} else if (im->key_repeat == 0) {
expand_notification_panel(controller);
} else {
expand_settings_panel(controller);
}
}
return;
@@ -666,7 +693,11 @@ input_manager_process_mouse_button(struct input_manager *im,
return;
}
if (control && event->button == SDL_BUTTON_X2 && down) {
expand_notification_panel(im->controller);
if (event->clicks < 2) {
expand_notification_panel(im->controller);
} else {
expand_settings_panel(im->controller);
}
return;
}
if (control && event->button == SDL_BUTTON_RIGHT) {

View File

@@ -33,6 +33,13 @@ struct input_manager {
} sdl_shortcut_mods;
bool vfinger_down;
// Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of
// system-generated repeated key presses.
unsigned key_repeat;
SDL_Keycode last_keycode;
uint16_t last_mod;
};
void

View File

@@ -58,7 +58,7 @@ get_server_path(void) {
LOGE("Could not get executable path, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return SERVER_FILENAME;
return strdup(SERVER_FILENAME);
}
char *dir = dirname(executable_path);
size_t dirlen = strlen(dir);
@@ -70,7 +70,7 @@ get_server_path(void) {
LOGE("Could not alloc server path string, "
"using " SERVER_FILENAME " from current directory");
free(executable_path);
return SERVER_FILENAME;
return strdup(SERVER_FILENAME);
}
memcpy(server_path, dir, dirlen);

View File

@@ -92,11 +92,11 @@ encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
// A packet was received
bool ok = write_packet(vs, packet);
av_packet_unref(packet);
if (!ok) {
LOGW("Could not send packet to v4l2 sink");
return false;
}
av_packet_unref(packet);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive v4l2 video packet: %d", ret);
return false;
@@ -125,6 +125,7 @@ run_v4l2_sink(void *data) {
video_buffer_consume(&vs->vb, vs->frame);
bool ok = encode_and_write_frame(vs, vs->frame);
av_frame_unref(vs->frame);
if (!ok) {
LOGE("Could not send frame to v4l2 sink");
break;

View File

@@ -177,9 +177,9 @@ static void test_serialize_expand_notification_panel(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_collapse_notification_panel(void) {
static void test_serialize_expand_settings_panel(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
};
unsigned char buf[CONTROL_MSG_MAX_SIZE];
@@ -187,7 +187,22 @@ static void test_serialize_collapse_notification_panel(void) {
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_collapse_panels(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS,
};
unsigned char buf[CONTROL_MSG_MAX_SIZE];
size_t size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
@@ -274,7 +289,8 @@ int main(int argc, char *argv[]) {
test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel();
test_serialize_collapse_notification_panel();
test_serialize_expand_settings_panel();
test_serialize_collapse_panels();
test_serialize_get_clipboard();
test_serialize_set_clipboard();
test_serialize_set_screen_power_mode();

View File

@@ -3,6 +3,10 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.io.File;
import java.io.IOException;
@@ -15,25 +19,123 @@ public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
// A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable {
public static final Creator<Config> CREATOR = new Creator<Config>() {
@Override
public Config createFromParcel(Parcel in) {
return new Config(in);
}
@Override
public Config[] newArray(int size) {
return new Config[size];
}
};
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
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() {
// not instantiable
}
public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, int displayId)
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode, powerOffScreen, displayId);
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(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen,
int displayId) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
restoreStayOn), String.valueOf(restoreNormalPowerMode), String.valueOf(powerOffScreen), String.valueOf(displayId)};
private static void startProcess(Config config) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
@@ -60,31 +162,27 @@ public final class CleanUp {
Ln.i("Cleaning up");
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
int displayId = Integer.parseInt(args[4]);
Config config = Config.fromBase64(args[0]);
if (disableShowTouches || restoreStayOn != -1) {
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (disableShowTouches) {
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
}
if (restoreStayOn != -1) {
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
}
}
}
if (Device.isScreenOn()) {
if (powerOffScreen) {
if (config.powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(displayId);
} else if (restoreNormalPowerMode) {
Device.powerOffScreen(config.displayId);
} else if (config.restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}

View File

@@ -11,11 +11,12 @@ public final class ControlMessage {
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10;
public static final int TYPE_EXPAND_SETTINGS_PANEL = 6;
public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11;
private int type;
private String text;

View File

@@ -77,7 +77,8 @@ public class ControlMessageReader {
msg = parseSetScreenPowerMode();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);

View File

@@ -55,7 +55,7 @@ public class Controller {
public void control() throws IOException {
// on start, power on the device
if (!Device.isScreenOn()) {
device.injectKeycode(KeyEvent.KEYCODE_POWER);
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
@@ -107,7 +107,10 @@ public class Controller {
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
Device.expandNotificationPanel();
break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
Device.expandSettingsPanel();
break;
case ControlMessage.TYPE_COLLAPSE_PANELS:
Device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
@@ -270,7 +273,7 @@ public class Controller {
if (keepPowerModeOff) {
schedulePowerModeOff();
}
return device.injectKeycode(KeyEvent.KEYCODE_POWER);
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
}
private boolean setClipboard(String text, boolean paste) {
@@ -281,7 +284,7 @@ public class Controller {
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE);
}
return ok;

View File

@@ -164,54 +164,39 @@ public final class Device {
return supportsInputEvents;
}
public static boolean injectEvent(InputEvent inputEvent, int mode, int displayId) {
public static boolean injectEvent(InputEvent inputEvent, int displayId) {
if (!supportsInputEvents(displayId)) {
return false;
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode);
}
public boolean injectEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
return injectEvent(inputEvent, mode, displayId);
}
public static boolean injectEventOnDisplay(InputEvent event, int displayId) {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, displayId);
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectEvent(InputEvent event) {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
return injectEvent(event, displayId);
}
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEventOnDisplay(event, displayId);
return injectEvent(event, displayId);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEvent(event);
return injectKeyEvent(action, keyCode, repeat, metaState, displayId);
}
public static boolean injectKeycode(int keyCode, int displayId) {
public static boolean pressReleaseKeycode(int keyCode, int displayId) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId);
}
public boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
public boolean pressReleaseKeycode(int keyCode) {
return pressReleaseKeycode(keyCode, displayId);
}
public static boolean isScreenOn() {
@@ -230,6 +215,10 @@ public final class Device {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
}
public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
}
public static void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
}
@@ -283,7 +272,7 @@ public final class Device {
if (!isScreenOn()) {
return true;
}
return injectKeycode(KeyEvent.KEYCODE_POWER, displayId);
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId);
}
/**

View File

@@ -50,7 +50,7 @@ public final class Server {
}
}
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true, options.getPowerOffScreenOnClose(), options.getDisplayId());
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
boolean tunnelForward = options.isTunnelForward();

View File

@@ -14,7 +14,7 @@ public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodLegacy;
private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod;
public ActivityManager(IInterface manager) {
@@ -29,7 +29,7 @@ public class ActivityManager {
} catch (NoSuchMethodException e) {
// old version
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
getContentProviderExternalMethodLegacy = true;
getContentProviderExternalMethodNewVersion = false;
}
}
return getContentProviderExternalMethod;
@@ -46,7 +46,7 @@ public class ActivityManager {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (!getContentProviderExternalMethodLegacy) {
if (getContentProviderExternalMethodNewVersion) {
// new version
args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else {

View File

@@ -11,6 +11,8 @@ public class StatusBarManager {
private final IInterface manager;
private Method expandNotificationsPanelMethod;
private Method expandSettingsPanelMethod;
private boolean expandSettingsPanelMethodNewVersion = true;
private Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) {
@@ -24,6 +26,20 @@ public class StatusBarManager {
return expandNotificationsPanelMethod;
}
private Method getExpandSettingsPanel() throws NoSuchMethodException {
if (expandSettingsPanelMethod == null) {
try {
// Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class);
} catch (NoSuchMethodException e) {
// old version
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel");
expandSettingsPanelMethodNewVersion = false;
}
}
return expandSettingsPanelMethod;
}
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
if (collapsePanelsMethod == null) {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
@@ -40,6 +56,21 @@ public class StatusBarManager {
}
}
public void expandSettingsPanel() {
try {
Method method = getExpandSettingsPanel();
if (expandSettingsPanelMethodNewVersion) {
// new version
method.invoke(manager, (Object) null);
} else {
// old version
method.invoke(manager);
}
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public void collapsePanels() {
try {
Method method = getCollapsePanelsMethod();

View File

@@ -182,19 +182,35 @@ public class ControlMessageReaderTest {
}
@Test
public void testParseCollapseNotificationPanelEvent() throws IOException {
public void testParseExpandSettingsPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL);
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
}
@Test
public void testParseCollapsePanelsEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
}
@Test