mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-08-30 14:16:56 +00:00
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=1920x1080 --start-app=org.videolan.vlc PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370> Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
This commit is contained in:
parent
9c9d92fb1c
commit
13ce277e1f
16 changed files with 227 additions and 9 deletions
|
@ -23,6 +23,7 @@ public final class ControlMessage {
|
|||
public static final int TYPE_UHID_INPUT = 13;
|
||||
public static final int TYPE_UHID_DESTROY = 14;
|
||||
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;
|
||||
|
||||
|
@ -155,6 +156,13 @@ public final class ControlMessage {
|
|||
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() {
|
||||
return type;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,8 @@ public class ControlMessageReader {
|
|||
return parseUhidInput();
|
||||
case ControlMessage.TYPE_UHID_DESTROY:
|
||||
return parseUhidDestroy();
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
return parseStartApp();
|
||||
default:
|
||||
throw new ControlProtocolException("Unknown event type: " + type);
|
||||
}
|
||||
|
@ -155,6 +157,11 @@ public class ControlMessageReader {
|
|||
return ControlMessage.createUhidDestroy(id);
|
||||
}
|
||||
|
||||
private ControlMessage parseStartApp() throws IOException {
|
||||
String name = parseString(1);
|
||||
return ControlMessage.createStartApp(name);
|
||||
}
|
||||
|
||||
private Position parsePosition() throws IOException {
|
||||
int x = dis.readInt();
|
||||
int y = dis.readInt();
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.genymobile.scrcpy.AndroidVersions;
|
|||
import com.genymobile.scrcpy.AsyncProcessor;
|
||||
import com.genymobile.scrcpy.CleanUp;
|
||||
import com.genymobile.scrcpy.device.Device;
|
||||
import com.genymobile.scrcpy.device.DeviceApp;
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
@ -22,6 +23,7 @@ import android.view.KeyEvent;
|
|||
import android.view.MotionEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
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 ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
private ExecutorService startAppExecutor;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
|
@ -79,6 +82,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||
|
||||
private final AtomicReference<DisplayData> displayData = new AtomicReference<>();
|
||||
private final Object displayDataAvailable = new Object(); // condition variable
|
||||
|
||||
private long lastTouchDown;
|
||||
private final PointersState pointersState = new PointersState();
|
||||
|
@ -128,7 +132,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
@Override
|
||||
public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) {
|
||||
DisplayData data = new DisplayData(virtualDisplayId, positionMapper);
|
||||
this.displayData.set(data);
|
||||
DisplayData old = this.displayData.getAndSet(data);
|
||||
if (old == null) {
|
||||
// The very first time the Controller is notified of a new virtual display
|
||||
synchronized (displayDataAvailable) {
|
||||
displayDataAvailable.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UhidManager getUhidManager() {
|
||||
|
@ -287,6 +297,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
openHardKeyboardSettings();
|
||||
break;
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
startAppAsync(msg.getText());
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
@ -570,4 +583,68 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||
|
||||
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.FakeContext;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.wrappers.ActivityManager;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.DisplayControl;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
@ -12,9 +13,11 @@ import com.genymobile.scrcpy.wrappers.WindowManager;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
|
@ -215,9 +218,7 @@ public final class Device {
|
|||
List<DeviceApp> apps = new ArrayList<>();
|
||||
PackageManager pm = FakeContext.get().getPackageManager();
|
||||
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
|
||||
String name = pm.getApplicationLabel(appInfo).toString();
|
||||
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||
apps.add(new DeviceApp(appInfo.packageName, name, system));
|
||||
apps.add(toApp(pm, appInfo));
|
||||
}
|
||||
|
||||
return apps;
|
||||
|
@ -243,4 +244,45 @@ public final class Device {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
public static String buildAppListMessage() {
|
||||
StringBuilder builder = new StringBuilder("List of apps:");
|
||||
|
||||
public static String buildAppListMessage() {
|
||||
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:
|
||||
// 1. system flag (system apps are before non-system apps)
|
||||
|
|
|
@ -118,8 +118,12 @@ public final class ActivityManager {
|
|||
return startActivityAsUserMethod;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public int startActivity(Intent intent) {
|
||||
return startActivity(intent, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public int startActivity(Intent intent, Bundle options) {
|
||||
try {
|
||||
Method method = getStartActivityAsUserMethod();
|
||||
return (int) method.invoke(
|
||||
|
@ -133,7 +137,7 @@ public final class ActivityManager {
|
|||
/* requestCode */ 0,
|
||||
/* startFlags */ 0,
|
||||
/* profilerInfo */ null,
|
||||
/* bOptions */ null,
|
||||
/* bOptions */ options,
|
||||
/* userId */ /* UserHandle.USER_CURRENT */ -2);
|
||||
} catch (Throwable e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
|
|
|
@ -399,6 +399,27 @@ public class ControlMessageReaderTest {
|
|||
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
|
||||
public void testMultiEvents() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue