mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-04-21 03:55:05 +00:00
Merge c559adfda3
into 5524f378c8
This commit is contained in:
commit
e33d0e887c
12 changed files with 408 additions and 11 deletions
83
README.md
83
README.md
|
@ -721,6 +721,89 @@ The target directory can be changed on start:
|
|||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
### Android features
|
||||
|
||||
#### Announce scrcpy state of execution
|
||||
|
||||
**(Advanced feature)**
|
||||
|
||||
Turn on the announcement of scrcpy current status.
|
||||
Those announcements are done using the [broadcast intents] feature of Android.
|
||||
If no value is provided with this argument, all intents are turned on.
|
||||
|
||||
[broadcast intents]: https://developer.android.com/reference/android/content/Intent
|
||||
|
||||
Currently, the only events that exist are:
|
||||
|
||||
| Option | Description | [Intent Action] | [Intent Extras]
|
||||
| ----------|:---------------------------------------------- |:---------------------------------|:---------------
|
||||
| `start` | scrcpy starts | `com.genymobile.scrcpy.START` | STARTUP: true
|
||||
| `socket` | a socket for the duration of scrcpy's run created | `com.genymobile.scrcpy.SOCKET` | SOCKET: int
|
||||
| `stop` | scrcpy stops (best effort) | `com.genymobile.scrcpy.STOP` | SHUTDOWN: true
|
||||
| `cleaned` | scrcpy has finished cleaning up (best effort) | `com.genymobile.scrcpy.CLEANED` | SHUTDOWN: true
|
||||
|
||||
[Intent Action]: https://developer.android.com/reference/android/content/Intent#setAction(java.lang.String)
|
||||
[Intent Extras]: https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable)
|
||||
|
||||
|
||||
**Important:**
|
||||
1. `stop` and `cleaned` **may not happen** in specific cases. For example,
|
||||
if debugging is turned off, scrcpy process is immediately killed without a chance to cleanup.
|
||||
2. The only guaranteed way to know if scrcpy has exited is by listening for a connection reset on
|
||||
the socket
|
||||
3. This option is intended **for advanced users**. By using this
|
||||
feature, all apps on your phone will know scrcpy has connected
|
||||
Unless that is what you want, and you know what that means
|
||||
do not use this feature
|
||||
4. In order for this argument to produce visible results you must create
|
||||
some automation to listen to android broadcast intents.
|
||||
Such as with your own app or with automation apps such as [Tasker].
|
||||
|
||||
|
||||
Following [Android intent rules], all intents fields/keys prefixed with:
|
||||
`com.genymobile.scrcpy.`
|
||||
In case of Actions, it is followed by the intent name in caps. For example,
|
||||
the 'start' intent has the action:
|
||||
`com.genymobile.scrcpy.START`
|
||||
|
||||
|
||||
[Android intent rules]: https://developer.android.com/reference/android/content/Intent#setAction(java.lang.String)
|
||||
|
||||
Additionally, there are two boolean fields (that may not be present) in the extra data section of the intents:
|
||||
|
||||
1. `com.genymobile.scrcpy.STARTUP` if present and `true`, scrcpy is starting up.
|
||||
2. `com.genymobile.scrcpy.SHUTDOWN` if present and `true`, scrcpy is shutting down.
|
||||
3. `com.genymobile.scrcpy.SOCKET` if present and an int, scrcpy has created a socket on the specified
|
||||
port and you can listen to it to confirm when scrcpy exits
|
||||
|
||||
More extra fields will be present in the future.
|
||||
|
||||
In case you listen to the socket provided by `com.genymobile.scrcpy.SOCKET`, note that **no information will
|
||||
be exchanged through it**. Even though bytes will be transmitted through it, they are only a test to
|
||||
ensure the connection is still alive and have no meaning.
|
||||
A connection reset followed by connection refused when trying to reestablish the connection is the
|
||||
only infallible way to ensure that scrcpy has turned off.
|
||||
|
||||
|
||||
For convinience with automation tools such as [Tasker], scrcpy also writes to the data field of the intents.
|
||||
The scheme is `scrcpy-status`.
|
||||
|
||||
[Tasker]: https://tasker.joaoapps.com/
|
||||
|
||||
**Example usages:**
|
||||
|
||||
```bash
|
||||
scrcpy --broadcast-intents
|
||||
```
|
||||
|
||||
```bash
|
||||
scrcpy --broadcast-intents=start
|
||||
```
|
||||
|
||||
```bash
|
||||
scrcpy --broadcast-intents start,cleaned
|
||||
```
|
||||
|
||||
|
||||
### Audio forwarding
|
||||
|
||||
|
|
|
@ -28,6 +28,45 @@ scrcpy_print_usage(const char *arg0) {
|
|||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
" Default is " STR(DEFAULT_BIT_RATE) ".\n"
|
||||
"\n"
|
||||
" --broadcast-intents [value[, ...]]\n"
|
||||
" (Advanced feature)\n"
|
||||
" Turn on the broadcast of intents with the status of scrcpy \n"
|
||||
" options are: start, socket, stop, cleaned\n"
|
||||
" Each of these will arm the corresponding intent\n"
|
||||
" start: announce finished setting up\n"
|
||||
" socket: announce isAlive server port\n"
|
||||
" stop: announce shut down started (best effort)\n"
|
||||
" cleaned: announce cleanup finished (best effort)\n"
|
||||
" \n"
|
||||
" If you ommit the value, all intents are turned on\n"
|
||||
" \n"
|
||||
" All intents have the action and extra fields prefixed with: \n"
|
||||
" com.genymobile.scrcpy.\n"
|
||||
" Which is then followed by the intent name in caps. For example,\n"
|
||||
" the 'start' intent has the action:\n"
|
||||
" com.genymobile.scrcpy.START\n"
|
||||
"\n"
|
||||
" There are two boolean extras use to ease\n"
|
||||
" the parsing process of the intents:\n"
|
||||
" 1. com.genymobile.scrcpy.STARTUP if present and true,\n"
|
||||
" scrcpy is starting up.\n"
|
||||
" 2. com.genymobile.scrcpy.SHUTDOWN if present and true,\n"
|
||||
" scrcpy is shutting down.\n"
|
||||
"\n"
|
||||
" socket has a different extra\n"
|
||||
" com.genymobile.scrcpy.SOCKET which is the port where the socket\n"
|
||||
" listens to.\n"
|
||||
" Listening for a connection reset on the socket is the only\n"
|
||||
" guaranteed way to know when scrcpy disconnects or crashes\n"
|
||||
" \n"
|
||||
" Notes:\n"
|
||||
" 1. stop and cleaned may not happen in specific cases. For example, \n"
|
||||
" if debugging is turned off, scrcpy process is immediately killed \n"
|
||||
" 2. This option is intended for advanced users. By using this \n"
|
||||
" feature, all apps on your phone will know scrcpy has connected\n"
|
||||
" Unless that is what you want, and you know what that means\n"
|
||||
" do not use this feature\n"
|
||||
"\n"
|
||||
" --codec-options key[:type]=value[,...]\n"
|
||||
" Set a list of comma-separated key:type=value options for the\n"
|
||||
" device encoder.\n"
|
||||
|
@ -661,6 +700,53 @@ guess_record_format(const char *filename) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
parse_intent_broadcast(const char *s, uint32_t *intents) {
|
||||
|
||||
// if no arg provided activates all intents for all intents and purposes
|
||||
if(!s){
|
||||
*intents = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
char *comma = strchr(s, ',');
|
||||
|
||||
assert(!comma || comma > s);
|
||||
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
|
||||
|
||||
|
||||
#define STREQ(literal, s, len) \
|
||||
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
|
||||
|
||||
if (STREQ("start", s, limit)) {
|
||||
*intents |= SC_INTENT_BROADCAST_START;
|
||||
} else if (STREQ("socket", s, limit)) {
|
||||
*intents |= SC_INTENT_BROADCAST_SOCKET;
|
||||
} else if (STREQ("stop", s, limit)) {
|
||||
*intents |= SC_INTENT_BROADCAST_STOP;
|
||||
} else if (STREQ("cleaned", s, limit)) {
|
||||
*intents |= SC_INTENT_BROADCAST_CLEANED;
|
||||
} else {
|
||||
LOGE("Unknown broadcast intent: %.*s "
|
||||
"(must be one of: start, socket, stop, cleaned)",
|
||||
(int) limit, s);
|
||||
return false;
|
||||
}
|
||||
#undef STREQ
|
||||
|
||||
if (!comma) {
|
||||
break;
|
||||
}
|
||||
|
||||
s = comma + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
||||
#define OPT_WINDOW_TITLE 1001
|
||||
#define OPT_PUSH_TARGET 1002
|
||||
|
@ -689,6 +775,7 @@ guess_record_format(const char *filename) {
|
|||
#define OPT_ENCODER_NAME 1025
|
||||
#define OPT_POWER_OFF_ON_CLOSE 1026
|
||||
#define OPT_V4L2_SINK 1027
|
||||
#define OPT_INTENT_BROADCAST 1028
|
||||
|
||||
bool
|
||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
|
@ -744,6 +831,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||
OPT_WINDOW_BORDERLESS},
|
||||
{"power-off-on-close", no_argument, NULL,
|
||||
OPT_POWER_OFF_ON_CLOSE},
|
||||
{"intent-broadcast", optional_argument, NULL,
|
||||
OPT_INTENT_BROADCAST},
|
||||
{NULL, 0, NULL, 0 },
|
||||
};
|
||||
|
||||
|
@ -922,6 +1011,12 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||
opts->v4l2_device = optarg;
|
||||
break;
|
||||
#endif
|
||||
|
||||
case OPT_INTENT_BROADCAST:
|
||||
if (!parse_intent_broadcast(optarg, &opts->intent_broadcasts)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
|
|
|
@ -279,6 +279,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||
.encoder_name = options->encoder_name,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
.intent_broadcasts = options->intent_broadcasts,
|
||||
};
|
||||
if (!server_start(&s->server, ¶ms)) {
|
||||
goto end;
|
||||
|
|
|
@ -54,6 +54,15 @@ struct sc_port_range {
|
|||
|
||||
#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
|
||||
|
||||
|
||||
enum sc_intent_broadcast {
|
||||
SC_INTENT_BROADCAST_START = 1 << 0,
|
||||
SC_INTENT_BROADCAST_SOCKET = 1 << 1,
|
||||
SC_INTENT_BROADCAST_STOP = 1 << 30,
|
||||
SC_INTENT_BROADCAST_CLEANED = 1 << 31,
|
||||
};
|
||||
|
||||
|
||||
struct scrcpy_options {
|
||||
const char *serial;
|
||||
const char *crop;
|
||||
|
@ -94,6 +103,7 @@ struct scrcpy_options {
|
|||
bool forward_all_clicks;
|
||||
bool legacy_paste;
|
||||
bool power_off_on_close;
|
||||
uint32_t intent_broadcasts;
|
||||
};
|
||||
|
||||
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||
|
@ -142,6 +152,7 @@ struct scrcpy_options {
|
|||
.forward_all_clicks = false, \
|
||||
.legacy_paste = false, \
|
||||
.power_off_on_close = false, \
|
||||
.intent_broadcasts = 0, \
|
||||
}
|
||||
|
||||
bool
|
||||
|
|
|
@ -258,11 +258,13 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||
char max_fps_string[6];
|
||||
char lock_video_orientation_string[5];
|
||||
char display_id_string[11];
|
||||
char intent_broadcasts_string[11];
|
||||
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
||||
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
||||
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
|
||||
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
|
||||
sprintf(display_id_string, "%"PRIu32, params->display_id);
|
||||
sprintf(intent_broadcasts_string, "%"PRIu32, params->intent_broadcasts);
|
||||
const char *const cmd[] = {
|
||||
"shell",
|
||||
"CLASSPATH=" DEVICE_SERVER_PATH,
|
||||
|
@ -296,6 +298,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||
params->codec_options ? params->codec_options : "-",
|
||||
params->encoder_name ? params->encoder_name : "-",
|
||||
params->power_off_on_close ? "true" : "false",
|
||||
intent_broadcasts_string,
|
||||
};
|
||||
#ifdef SERVER_DEBUGGER
|
||||
LOGI("Server debugger waiting for a client on device port "
|
||||
|
|
|
@ -49,6 +49,7 @@ struct server_params {
|
|||
bool stay_awake;
|
||||
bool force_adb_forward;
|
||||
bool power_off_on_close;
|
||||
uint32_t intent_broadcasts;
|
||||
};
|
||||
|
||||
// init default values
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
|
@ -34,9 +36,10 @@ public final class CleanUp {
|
|||
}
|
||||
};
|
||||
|
||||
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 static final int FLAG_DISABLE_SHOW_TOUCHES = 1 << 0;
|
||||
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 1 << 1;
|
||||
private static final int FLAG_POWER_OFF_SCREEN = 1 << 2;
|
||||
private static final int FLAG_BROADCAST_CLEANED = 1 << 3;
|
||||
|
||||
private int displayId;
|
||||
|
||||
|
@ -47,6 +50,7 @@ public final class CleanUp {
|
|||
private boolean disableShowTouches;
|
||||
private boolean restoreNormalPowerMode;
|
||||
private boolean powerOffScreen;
|
||||
private boolean broadcastCleaned;
|
||||
|
||||
public Config() {
|
||||
// Default constructor, the fields are initialized by CleanUp.configure()
|
||||
|
@ -59,6 +63,7 @@ public final class CleanUp {
|
|||
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
|
||||
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
|
||||
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
|
||||
broadcastCleaned = (options & FLAG_BROADCAST_CLEANED) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,11 +80,14 @@ public final class CleanUp {
|
|||
if (powerOffScreen) {
|
||||
options |= FLAG_POWER_OFF_SCREEN;
|
||||
}
|
||||
if (broadcastCleaned) {
|
||||
options |= FLAG_BROADCAST_CLEANED;
|
||||
}
|
||||
dest.writeByte(options);
|
||||
}
|
||||
|
||||
private boolean hasWork() {
|
||||
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
|
||||
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen || broadcastCleaned;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -117,7 +125,8 @@ public final class CleanUp {
|
|||
// not instantiable
|
||||
}
|
||||
|
||||
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
|
||||
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode,
|
||||
boolean powerOffScreen, boolean broadcastCleaned)
|
||||
throws IOException {
|
||||
Config config = new Config();
|
||||
config.displayId = displayId;
|
||||
|
@ -125,6 +134,7 @@ public final class CleanUp {
|
|||
config.restoreStayOn = restoreStayOn;
|
||||
config.restoreNormalPowerMode = restoreNormalPowerMode;
|
||||
config.powerOffScreen = powerOffScreen;
|
||||
config.broadcastCleaned = broadcastCleaned;
|
||||
|
||||
if (config.hasWork()) {
|
||||
startProcess(config);
|
||||
|
@ -187,5 +197,19 @@ public final class CleanUp {
|
|||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
if(config.broadcastCleaned){
|
||||
Ln.i("Announce cleaned");
|
||||
announceScrcpyCleaned();
|
||||
}
|
||||
}
|
||||
|
||||
private static void announceScrcpyCleaned() {
|
||||
|
||||
Intent cleaned = new Intent(Intents.scrcpyPrefix("CLEANED"));
|
||||
cleaned.setData(Uri.parse("scrcpy-status:cleaned"));
|
||||
cleaned.putExtra(Intents.scrcpyPrefix("STARTUP"), false);
|
||||
cleaned.putExtra(Intents.scrcpyPrefix("SHUTDOWN"), true);
|
||||
Device.sendBroadcast(cleaned);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
|||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
@ -299,4 +300,8 @@ public final class Device {
|
|||
public static ContentProvider createSettingsProvider() {
|
||||
return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
|
||||
}
|
||||
|
||||
public static void sendBroadcast(Intent intent) {
|
||||
SERVICE_MANAGER.getActivityManager().sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
|
37
server/src/main/java/com/genymobile/scrcpy/Intents.java
Normal file
37
server/src/main/java/com/genymobile/scrcpy/Intents.java
Normal file
|
@ -0,0 +1,37 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
enum Intents {
|
||||
START(1),
|
||||
SOCKET(2),
|
||||
STOP(30),
|
||||
CLEANED(31),
|
||||
;
|
||||
|
||||
public static final String SCRCPY_PREFIX = "com.genymobile.scrcpy.";
|
||||
|
||||
int shift;
|
||||
Intents(int shift) {
|
||||
this.shift = shift;
|
||||
}
|
||||
|
||||
public static EnumSet<Intents> fromBitSet(BitSet bits) {
|
||||
EnumSet<Intents> es = EnumSet.allOf(Intents.class);
|
||||
|
||||
Iterator<Intents> it = es.iterator();
|
||||
Intents intent;
|
||||
while (it.hasNext()) {
|
||||
intent = it.next();
|
||||
if (!bits.get(intent.shift - 1)) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
return es;
|
||||
}
|
||||
|
||||
public static String scrcpyPrefix(String unprefixed){
|
||||
return SCRCPY_PREFIX + unprefixed;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,9 @@ package com.genymobile.scrcpy;
|
|||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.EnumSet;
|
||||
|
||||
public class Options {
|
||||
private Ln.Level logLevel;
|
||||
private int maxSize;
|
||||
|
@ -18,6 +21,7 @@ public class Options {
|
|||
private String codecOptions;
|
||||
private String encoderName;
|
||||
private boolean powerOffScreenOnClose;
|
||||
private EnumSet<Intents> broadcastIntents;
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
|
@ -83,6 +87,10 @@ public class Options {
|
|||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
public void setBroadcastIntents(EnumSet<Intents> broadcastIntents) {
|
||||
this.broadcastIntents = broadcastIntents;
|
||||
}
|
||||
|
||||
public boolean getControl() {
|
||||
return control;
|
||||
}
|
||||
|
@ -138,4 +146,8 @@ public class Options {
|
|||
public boolean getPowerOffScreenOnClose() {
|
||||
return this.powerOffScreenOnClose;
|
||||
}
|
||||
|
||||
public EnumSet<Intents> getBroadcastIntents() {
|
||||
return broadcastIntents;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.*;
|
||||
|
||||
public final class Server {
|
||||
|
||||
|
@ -50,7 +54,8 @@ public final class Server {
|
|||
}
|
||||
}
|
||||
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose(),
|
||||
options.getBroadcastIntents().contains(Intents.CLEANED));
|
||||
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
|
||||
|
@ -75,6 +80,13 @@ public final class Server {
|
|||
});
|
||||
}
|
||||
|
||||
if(options.getBroadcastIntents().contains(Intents.START)){
|
||||
announceScrcpyStarting();
|
||||
}
|
||||
if(options.getBroadcastIntents().contains(Intents.SOCKET)){
|
||||
scrcpyRunningSocket();
|
||||
}
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen(device, connection.getVideoFd());
|
||||
|
@ -88,10 +100,92 @@ public final class Server {
|
|||
if (deviceMessageSenderThread != null) {
|
||||
deviceMessageSenderThread.interrupt();
|
||||
}
|
||||
|
||||
if(options.getBroadcastIntents().contains(Intents.STOP)){
|
||||
Ln.i("Announcing stopped");
|
||||
announceScrcpyStopping();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void announceScrcpyStarting() {
|
||||
|
||||
Intent starting = new Intent(Intents.scrcpyPrefix("START"));
|
||||
starting.setData(Uri.parse("scrcpy-status:start"));
|
||||
starting.putExtra(Intents.scrcpyPrefix("STARTUP"), true);
|
||||
starting.putExtra(Intents.scrcpyPrefix("SHUTDOWN"), false);
|
||||
Device.sendBroadcast(starting);
|
||||
}
|
||||
private static void announceScrcpyStopping() {
|
||||
|
||||
Intent stopping = new Intent(Intents.scrcpyPrefix("STOP"));
|
||||
stopping.setData(Uri.parse("scrcpy-status:stop"));
|
||||
stopping.putExtra(Intents.scrcpyPrefix("STARTUP"), false);
|
||||
stopping.putExtra(Intents.scrcpyPrefix("SHUTDOWN"), true);
|
||||
Device.sendBroadcast(stopping);
|
||||
}
|
||||
|
||||
private static Thread scrcpyRunningSocket() {
|
||||
|
||||
// Thread runs until scrcpy exits and doesn't block exiting
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
while(true) {
|
||||
|
||||
ArrayList<Socket> acceptedSockets = new ArrayList<>();
|
||||
try (ServerSocket ss = new ServerSocket(0, -1, InetAddress.getLocalHost())) {
|
||||
int localPort = ss.getLocalPort();
|
||||
Ln.i("Running socket on " + localPort);
|
||||
|
||||
Intent starting = new Intent(Intents.scrcpyPrefix("SOCKET"));
|
||||
starting.setData(Uri.parse("scrcpy-status:socket"));
|
||||
starting.putExtra(Intents.scrcpyPrefix("SOCKET"), localPort);
|
||||
Device.sendBroadcast(starting);
|
||||
|
||||
while (true) {
|
||||
Socket accepted = ss.accept();
|
||||
if (acceptedSockets.size() < 50) {
|
||||
acceptedSockets.add(accepted);
|
||||
Ln.d("Running socket: Added listener");
|
||||
}
|
||||
ListIterator<Socket> iter = acceptedSockets.listIterator();
|
||||
for (Socket s = iter.next(); iter.hasNext(); s = iter.next()) {
|
||||
try {
|
||||
s.getOutputStream().write(1);
|
||||
} catch (SocketException e) {
|
||||
iter.remove();
|
||||
Ln.d("Running socket: Removed listener");
|
||||
try {
|
||||
s.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Ln.e("Running socket: Cannot start server", e);
|
||||
} catch (Exception e) {
|
||||
Ln.e("Running socket: failed with an unexpected exception", e);
|
||||
}
|
||||
|
||||
try {
|
||||
// Avoid wasting resources in case of infinite loop
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.setName("Running socket");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
|
@ -135,7 +229,7 @@ public final class Server {
|
|||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||
}
|
||||
|
||||
final int expectedParameters = 16;
|
||||
final int expectedParameters = 17;
|
||||
if (args.length != expectedParameters) {
|
||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
||||
}
|
||||
|
@ -188,6 +282,9 @@ public final class Server {
|
|||
boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]);
|
||||
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
||||
|
||||
EnumSet<Intents> broadcastIntents = Intents.fromBitSet(BitSet.valueOf(new long[]{Long.parseLong(args[16])}));
|
||||
options.setBroadcastIntents(broadcastIntents);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@ package com.genymobile.scrcpy.wrappers;
|
|||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
|
||||
|
@ -16,6 +18,7 @@ public class ActivityManager {
|
|||
private Method getContentProviderExternalMethod;
|
||||
private boolean getContentProviderExternalMethodNewVersion = true;
|
||||
private Method removeContentProviderExternalMethod;
|
||||
private Method broadcastIntentMethod;
|
||||
|
||||
public ActivityManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
|
@ -42,6 +45,22 @@ public class ActivityManager {
|
|||
return removeContentProviderExternalMethod;
|
||||
}
|
||||
|
||||
private Method getBroadcastIntentMethod() throws NoSuchMethodException {
|
||||
if (broadcastIntentMethod == null) {
|
||||
try {
|
||||
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
|
||||
Class<?> iIntentReceiverClass = Class.forName("android.content.IIntentReceiver");
|
||||
broadcastIntentMethod = manager.getClass()
|
||||
.getMethod("broadcastIntent", iApplicationThreadClass, Intent.class, String.class, iIntentReceiverClass, int.class,
|
||||
String.class, Bundle.class, String[].class, int.class, Bundle.class, boolean.class, boolean.class, int.class);
|
||||
return broadcastIntentMethod;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
return broadcastIntentMethod;
|
||||
}
|
||||
|
||||
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
||||
try {
|
||||
Method method = getGetContentProviderExternalMethod();
|
||||
|
@ -84,4 +103,13 @@ public class ActivityManager {
|
|||
public ContentProvider createSettingsProvider() {
|
||||
return getContentProviderExternal("settings", new Binder());
|
||||
}
|
||||
|
||||
public void sendBroadcast(Intent intent) {
|
||||
try {
|
||||
Method method = getBroadcastIntentMethod();
|
||||
method.invoke(manager, null, intent, null, null, 0, null, null, null, -1, null, true, false, ServiceManager.USER_ID);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue