Add --start-app
Add a command line option --start-app=name to start an Android app by its package name. For example: scrcpy --start-app=org.mozilla.firefox The app will be started on the correct target display: scrcpy --new-display --start-app=org.mozilla.firefox Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
This commit is contained in:
parent
6ff3d9b571
commit
d2989920c1
@ -79,6 +79,7 @@ _scrcpy() {
|
|||||||
-s --serial=
|
-s --serial=
|
||||||
-S --turn-screen-off
|
-S --turn-screen-off
|
||||||
--shortcut-mod=
|
--shortcut-mod=
|
||||||
|
--start-app=
|
||||||
-t --show-touches
|
-t --show-touches
|
||||||
--tcpip
|
--tcpip
|
||||||
--tcpip=
|
--tcpip=
|
||||||
|
@ -82,6 +82,7 @@ arguments=(
|
|||||||
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
||||||
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
|
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
|
||||||
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
||||||
|
'--start-app=[Start an Android app]'
|
||||||
{-t,--show-touches}'[Show physical touches]'
|
{-t,--show-touches}'[Show physical touches]'
|
||||||
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
|
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
|
||||||
'--time-limit=[Set the maximum mirroring time, in seconds]'
|
'--time-limit=[Set the maximum mirroring time, in seconds]'
|
||||||
|
@ -493,6 +493,10 @@ For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsu
|
|||||||
|
|
||||||
Default is "lalt,lsuper" (left-Alt or left-Super).
|
Default is "lalt,lsuper" (left-Alt or left-Super).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-start\-app " name
|
||||||
|
Start an Android app, by its exact package name.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-t, \-\-show\-touches
|
.B \-t, \-\-show\-touches
|
||||||
Enable "show touches" on start, restore the initial value on exit.
|
Enable "show touches" on start, restore the initial value on exit.
|
||||||
|
@ -104,6 +104,7 @@ enum {
|
|||||||
OPT_GAMEPAD,
|
OPT_GAMEPAD,
|
||||||
OPT_NEW_DISPLAY,
|
OPT_NEW_DISPLAY,
|
||||||
OPT_LIST_APPS,
|
OPT_LIST_APPS,
|
||||||
|
OPT_START_APP,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
@ -805,6 +806,12 @@ static const struct sc_option options[] = {
|
|||||||
"shortcuts, pass \"lctrl,lsuper\".\n"
|
"shortcuts, pass \"lctrl,lsuper\".\n"
|
||||||
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
|
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_START_APP,
|
||||||
|
.longopt = "start-app",
|
||||||
|
.argdesc = "name",
|
||||||
|
.text = "Start an Android app, by its exact package name.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.shortopt = 't',
|
.shortopt = 't',
|
||||||
.longopt = "show-touches",
|
.longopt = "show-touches",
|
||||||
@ -2695,6 +2702,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
case OPT_NEW_DISPLAY:
|
case OPT_NEW_DISPLAY:
|
||||||
opts->new_display = optarg ? optarg : "auto";
|
opts->new_display = optarg ? optarg : "auto";
|
||||||
break;
|
break;
|
||||||
|
case OPT_START_APP:
|
||||||
|
opts->start_app = optarg;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
@ -3123,6 +3133,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
LOGE("Cannot request power off on close if control is disabled");
|
LOGE("Cannot request power off on close if control is disabled");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (opts->start_app) {
|
||||||
|
LOGE("Cannot start an Android app if control is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ifdef _WIN32
|
# ifdef _WIN32
|
||||||
|
@ -183,6 +183,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
|||||||
case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
|
case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
|
||||||
sc_write16be(&buf[1], msg->uhid_destroy.id);
|
sc_write16be(&buf[1], msg->uhid_destroy.id);
|
||||||
return 3;
|
return 3;
|
||||||
|
case SC_CONTROL_MSG_TYPE_START_APP: {
|
||||||
|
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
|
||||||
|
return 1 + len;
|
||||||
|
}
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
||||||
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
||||||
@ -308,6 +312,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
|||||||
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||||
LOG_CMSG("open hard keyboard settings");
|
LOG_CMSG("open hard keyboard settings");
|
||||||
break;
|
break;
|
||||||
|
case SC_CONTROL_MSG_TYPE_START_APP:
|
||||||
|
LOG_CMSG("start app \"%s\"", msg->start_app.name);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
||||||
break;
|
break;
|
||||||
@ -333,6 +340,9 @@ sc_control_msg_destroy(struct sc_control_msg *msg) {
|
|||||||
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
||||||
free(msg->set_clipboard.text);
|
free(msg->set_clipboard.text);
|
||||||
break;
|
break;
|
||||||
|
case SC_CONTROL_MSG_TYPE_START_APP:
|
||||||
|
free(msg->start_app.name);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
break;
|
break;
|
||||||
|
@ -41,6 +41,7 @@ enum sc_control_msg_type {
|
|||||||
SC_CONTROL_MSG_TYPE_UHID_INPUT,
|
SC_CONTROL_MSG_TYPE_UHID_INPUT,
|
||||||
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
|
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
|
||||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
||||||
|
SC_CONTROL_MSG_TYPE_START_APP,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_screen_power_mode {
|
enum sc_screen_power_mode {
|
||||||
@ -110,6 +111,9 @@ struct sc_control_msg {
|
|||||||
struct {
|
struct {
|
||||||
uint16_t id;
|
uint16_t id;
|
||||||
} uhid_destroy;
|
} uhid_destroy;
|
||||||
|
struct {
|
||||||
|
char *name;
|
||||||
|
} start_app;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.mouse_hover = true,
|
.mouse_hover = true,
|
||||||
.audio_dup = false,
|
.audio_dup = false,
|
||||||
.new_display = NULL,
|
.new_display = NULL,
|
||||||
|
.start_app = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_orientation
|
enum sc_orientation
|
||||||
|
@ -310,6 +310,7 @@ struct scrcpy_options {
|
|||||||
bool mouse_hover;
|
bool mouse_hover;
|
||||||
bool audio_dup;
|
bool audio_dup;
|
||||||
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
||||||
|
const char *start_app;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern const struct scrcpy_options scrcpy_options_default;
|
extern const struct scrcpy_options scrcpy_options_default;
|
||||||
|
@ -907,6 +907,25 @@ aoa_complete:
|
|||||||
init_sdl_gamepads();
|
init_sdl_gamepads();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options->control && options->start_app) {
|
||||||
|
assert(controller);
|
||||||
|
|
||||||
|
char *name = strdup(options->start_app);
|
||||||
|
if (!name) {
|
||||||
|
LOG_OOM();
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sc_control_msg msg;
|
||||||
|
msg.type = SC_CONTROL_MSG_TYPE_START_APP;
|
||||||
|
msg.start_app.name = name;
|
||||||
|
|
||||||
|
if (!sc_controller_push_msg(controller, &msg)) {
|
||||||
|
LOGW("Could not request start app '%s'", name);
|
||||||
|
free(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ret = event_loop(s);
|
ret = event_loop(s);
|
||||||
terminate_event_loop();
|
terminate_event_loop();
|
||||||
LOGD("quit...");
|
LOGD("quit...");
|
||||||
|
@ -23,6 +23,7 @@ public final class ControlMessage {
|
|||||||
public static final int TYPE_UHID_INPUT = 13;
|
public static final int TYPE_UHID_INPUT = 13;
|
||||||
public static final int TYPE_UHID_DESTROY = 14;
|
public static final int TYPE_UHID_DESTROY = 14;
|
||||||
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
||||||
|
public static final int TYPE_START_APP = 16;
|
||||||
|
|
||||||
public static final long SEQUENCE_INVALID = 0;
|
public static final long SEQUENCE_INVALID = 0;
|
||||||
|
|
||||||
@ -155,6 +156,13 @@ public final class ControlMessage {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ControlMessage createStartApp(String name) {
|
||||||
|
ControlMessage msg = new ControlMessage();
|
||||||
|
msg.type = TYPE_START_APP;
|
||||||
|
msg.text = name;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
public int getType() {
|
public int getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ public class ControlMessageReader {
|
|||||||
return parseUhidInput();
|
return parseUhidInput();
|
||||||
case ControlMessage.TYPE_UHID_DESTROY:
|
case ControlMessage.TYPE_UHID_DESTROY:
|
||||||
return parseUhidDestroy();
|
return parseUhidDestroy();
|
||||||
|
case ControlMessage.TYPE_START_APP:
|
||||||
|
return parseStartApp();
|
||||||
default:
|
default:
|
||||||
throw new ControlProtocolException("Unknown event type: " + type);
|
throw new ControlProtocolException("Unknown event type: " + type);
|
||||||
}
|
}
|
||||||
@ -155,6 +157,11 @@ public class ControlMessageReader {
|
|||||||
return ControlMessage.createUhidDestroy(id);
|
return ControlMessage.createUhidDestroy(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ControlMessage parseStartApp() throws IOException {
|
||||||
|
String name = parseString(1);
|
||||||
|
return ControlMessage.createStartApp(name);
|
||||||
|
}
|
||||||
|
|
||||||
private Position parsePosition() throws IOException {
|
private Position parsePosition() throws IOException {
|
||||||
int x = dis.readInt();
|
int x = dis.readInt();
|
||||||
int y = dis.readInt();
|
int y = dis.readInt();
|
||||||
|
@ -4,6 +4,7 @@ import com.genymobile.scrcpy.AndroidVersions;
|
|||||||
import com.genymobile.scrcpy.AsyncProcessor;
|
import com.genymobile.scrcpy.AsyncProcessor;
|
||||||
import com.genymobile.scrcpy.CleanUp;
|
import com.genymobile.scrcpy.CleanUp;
|
||||||
import com.genymobile.scrcpy.device.Device;
|
import com.genymobile.scrcpy.device.Device;
|
||||||
|
import com.genymobile.scrcpy.device.DeviceApp;
|
||||||
import com.genymobile.scrcpy.device.Point;
|
import com.genymobile.scrcpy.device.Point;
|
||||||
import com.genymobile.scrcpy.device.Position;
|
import com.genymobile.scrcpy.device.Position;
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
@ -22,6 +23,7 @@ import android.view.KeyEvent;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -61,6 +63,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
private static final int POINTER_ID_MOUSE = -1;
|
private static final int POINTER_ID_MOUSE = -1;
|
||||||
|
|
||||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
private ExecutorService startAppExecutor;
|
||||||
|
|
||||||
private Thread thread;
|
private Thread thread;
|
||||||
|
|
||||||
@ -79,6 +82,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||||
|
|
||||||
private final AtomicReference<DisplayData> displayData = new AtomicReference<>();
|
private final AtomicReference<DisplayData> displayData = new AtomicReference<>();
|
||||||
|
private final Object displayDataAvailable = new Object(); // condition variable
|
||||||
|
|
||||||
private long lastTouchDown;
|
private long lastTouchDown;
|
||||||
private final PointersState pointersState = new PointersState();
|
private final PointersState pointersState = new PointersState();
|
||||||
@ -129,7 +133,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) {
|
public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) {
|
||||||
DisplayData data = new DisplayData(virtualDisplayId, positionMapper);
|
DisplayData data = new DisplayData(virtualDisplayId, positionMapper);
|
||||||
this.displayData.set(data);
|
DisplayData old = this.displayData.getAndSet(data);
|
||||||
|
if (old == null) {
|
||||||
|
synchronized (displayDataAvailable) {
|
||||||
|
displayDataAvailable.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private UhidManager getUhidManager() {
|
private UhidManager getUhidManager() {
|
||||||
@ -288,6 +297,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||||
openHardKeyboardSettings();
|
openHardKeyboardSettings();
|
||||||
break;
|
break;
|
||||||
|
case ControlMessage.TYPE_START_APP:
|
||||||
|
startAppAsync(msg.getText());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
@ -571,4 +583,68 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
|
|
||||||
return data.virtualDisplayId;
|
return data.virtualDisplayId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startAppAsync(String name) {
|
||||||
|
if (startAppExecutor == null) {
|
||||||
|
startAppExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listing and selecting the app may take a lot of time
|
||||||
|
startAppExecutor.submit(() -> startApp(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startApp(String name) {
|
||||||
|
DeviceApp app = Device.findByPackageName(name);
|
||||||
|
if (app == null) {
|
||||||
|
Ln.w("No app found for package \"" + name + "\"");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int startAppDisplayId = getStartAppDisplayId();
|
||||||
|
if (startAppDisplayId == Device.DISPLAY_ID_NONE) {
|
||||||
|
Ln.e("No known display id to start app \"" + name + "\"");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "...");
|
||||||
|
Device.startApp(app.getPackageName(), startAppDisplayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getStartAppDisplayId() {
|
||||||
|
if (displayId != Device.DISPLAY_ID_NONE) {
|
||||||
|
return displayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirroring a new virtual display id (using --new-display-id feature)
|
||||||
|
try {
|
||||||
|
// Wait for at most 1 second until a virtual display id is known
|
||||||
|
DisplayData data = waitDisplayData(1000);
|
||||||
|
if (data != null) {
|
||||||
|
return data.virtualDisplayId;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
// No display id available
|
||||||
|
return Device.DISPLAY_ID_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DisplayData waitDisplayData(long timeoutMillis) throws InterruptedException {
|
||||||
|
long deadline = System.currentTimeMillis() + timeoutMillis;
|
||||||
|
|
||||||
|
synchronized (displayDataAvailable) {
|
||||||
|
DisplayData data = displayData.get();
|
||||||
|
while (data == null) {
|
||||||
|
long timeout = deadline - System.currentTimeMillis();
|
||||||
|
if (timeout < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
displayDataAvailable.wait(timeout);
|
||||||
|
data = displayData.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package com.genymobile.scrcpy.device;
|
|||||||
import com.genymobile.scrcpy.AndroidVersions;
|
import com.genymobile.scrcpy.AndroidVersions;
|
||||||
import com.genymobile.scrcpy.FakeContext;
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
|
import com.genymobile.scrcpy.wrappers.ActivityManager;
|
||||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||||
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
||||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||||
@ -12,9 +13,11 @@ import com.genymobile.scrcpy.wrappers.WindowManager;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.app.ActivityOptions;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
@ -214,9 +217,7 @@ public final class Device {
|
|||||||
List<DeviceApp> apps = new ArrayList<>();
|
List<DeviceApp> apps = new ArrayList<>();
|
||||||
PackageManager pm = FakeContext.get().getPackageManager();
|
PackageManager pm = FakeContext.get().getPackageManager();
|
||||||
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
|
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
|
||||||
String name = pm.getApplicationLabel(appInfo).toString();
|
apps.add(toApp(pm, appInfo));
|
||||||
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
|
||||||
apps.add(new DeviceApp(appInfo.packageName, name, system, appInfo.enabled));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return apps;
|
return apps;
|
||||||
@ -242,4 +243,45 @@ public final class Device {
|
|||||||
|
|
||||||
return pm.getLeanbackLaunchIntentForPackage(packageName);
|
return pm.getLeanbackLaunchIntentForPackage(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) {
|
||||||
|
String name = pm.getApplicationLabel(appInfo).toString();
|
||||||
|
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||||
|
return new DeviceApp(appInfo.packageName, name, system, appInfo.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
|
public static DeviceApp findByPackageName(String packageName) {
|
||||||
|
PackageManager pm = FakeContext.get().getPackageManager();
|
||||||
|
// No need to filter by "launchable" apps, an error will be reported on start if the app is not launchable
|
||||||
|
for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) {
|
||||||
|
if (packageName.equals(appInfo.packageName)) {
|
||||||
|
return toApp(pm, appInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startApp(String packageName, int displayId) {
|
||||||
|
PackageManager pm = FakeContext.get().getPackageManager();
|
||||||
|
|
||||||
|
Intent launchIntent = getLaunchIntent(pm, packageName);
|
||||||
|
if (launchIntent == null) {
|
||||||
|
Ln.w("Cannot create launch intent for app " + packageName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
Bundle options = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_26_ANDROID_8_0) {
|
||||||
|
ActivityOptions launchOptions = ActivityOptions.makeBasic();
|
||||||
|
launchOptions.setLaunchDisplayId(displayId);
|
||||||
|
options = launchOptions.toBundle();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivityManager am = ServiceManager.getActivityManager();
|
||||||
|
am.startActivity(launchIntent, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,11 +160,15 @@ public final class LogUtils {
|
|||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("QueryPermissionsNeeded")
|
|
||||||
public static String buildAppListMessage() {
|
|
||||||
StringBuilder builder = new StringBuilder("List of apps:");
|
|
||||||
|
|
||||||
|
public static String buildAppListMessage() {
|
||||||
List<DeviceApp> apps = Device.listApps();
|
List<DeviceApp> apps = Device.listApps();
|
||||||
|
return buildAppListMessage("List of apps:", apps);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
|
public static String buildAppListMessage(String title, List<DeviceApp> apps) {
|
||||||
|
StringBuilder builder = new StringBuilder(title);
|
||||||
|
|
||||||
// Sort by:
|
// Sort by:
|
||||||
// 1. system flag (system apps are before non-system apps)
|
// 1. system flag (system apps are before non-system apps)
|
||||||
|
@ -118,8 +118,12 @@ public final class ActivityManager {
|
|||||||
return startActivityAsUserMethod;
|
return startActivityAsUserMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
public int startActivity(Intent intent) {
|
public int startActivity(Intent intent) {
|
||||||
|
return startActivity(intent, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
public int startActivity(Intent intent, Bundle options) {
|
||||||
try {
|
try {
|
||||||
Method method = getStartActivityAsUserMethod();
|
Method method = getStartActivityAsUserMethod();
|
||||||
return (int) method.invoke(
|
return (int) method.invoke(
|
||||||
@ -133,7 +137,7 @@ public final class ActivityManager {
|
|||||||
/* requestCode */ 0,
|
/* requestCode */ 0,
|
||||||
/* startFlags */ 0,
|
/* startFlags */ 0,
|
||||||
/* profilerInfo */ null,
|
/* profilerInfo */ null,
|
||||||
/* bOptions */ null,
|
/* bOptions */ options,
|
||||||
/* userId */ /* UserHandle.USER_CURRENT */ -2);
|
/* userId */ /* UserHandle.USER_CURRENT */ -2);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
|
@ -399,6 +399,27 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(-1, bis.read()); // EOS
|
Assert.assertEquals(-1, bis.read()); // EOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseStartApp() throws IOException {
|
||||||
|
byte[] name = "firefox".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
|
dos.writeByte(ControlMessage.TYPE_START_APP);
|
||||||
|
dos.writeByte(name.length);
|
||||||
|
dos.write(name);
|
||||||
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||||
|
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||||
|
|
||||||
|
ControlMessage event = reader.read();
|
||||||
|
Assert.assertEquals(ControlMessage.TYPE_START_APP, event.getType());
|
||||||
|
Assert.assertEquals("firefox", event.getText());
|
||||||
|
|
||||||
|
Assert.assertEquals(-1, bis.read()); // EOS
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultiEvents() throws IOException {
|
public void testMultiEvents() throws IOException {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user