Add --list-apps

Add an option to list all apps installed on the device:

    scrcpy --list-apps

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
This commit is contained in:
Romain Vimont 2024-10-19 17:16:08 +02:00
parent 408a388fc5
commit 9c9d92fb1c
11 changed files with 154 additions and 1 deletions

View File

@ -33,6 +33,7 @@ _scrcpy() {
--keyboard= --keyboard=
--kill-adb-on-close --kill-adb-on-close
--legacy-paste --legacy-paste
--list-apps
--list-camera-sizes --list-camera-sizes
--list-cameras --list-cameras
--list-displays --list-displays

View File

@ -40,6 +40,7 @@ arguments=(
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-apps[List Android apps installed on the device]'
'--list-camera-sizes[List the valid camera capture sizes]' '--list-camera-sizes[List the valid camera capture sizes]'
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'

View File

@ -227,6 +227,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.B \-\-list\-apps
List Android apps installed on the device.
.TP .TP
.B \-\-list\-camera\-sizes .B \-\-list\-camera\-sizes
List the valid camera capture sizes. List the valid camera capture sizes.

View File

@ -103,6 +103,7 @@ enum {
OPT_AUDIO_DUP, OPT_AUDIO_DUP,
OPT_GAMEPAD, OPT_GAMEPAD,
OPT_NEW_DISPLAY, OPT_NEW_DISPLAY,
OPT_LIST_APPS,
}; };
struct sc_option { struct sc_option {
@ -443,6 +444,11 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as " "This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.", "expected when setting the device clipboard programmatically.",
}, },
{
.longopt_id = OPT_LIST_APPS,
.longopt = "list-apps",
.text = "List Android apps installed on the device.",
},
{ {
.longopt_id = OPT_LIST_CAMERAS, .longopt_id = OPT_LIST_CAMERAS,
.longopt = "list-cameras", .longopt = "list-cameras",
@ -2611,6 +2617,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_LIST_CAMERA_SIZES: case OPT_LIST_CAMERA_SIZES:
opts->list |= SC_OPTION_LIST_CAMERA_SIZES; opts->list |= SC_OPTION_LIST_CAMERA_SIZES;
break; break;
case OPT_LIST_APPS:
opts->list |= SC_OPTION_LIST_APPS;
break;
case OPT_REQUIRE_AUDIO: case OPT_REQUIRE_AUDIO:
opts->require_audio = true; opts->require_audio = true;
break; break;

View File

@ -304,6 +304,7 @@ struct scrcpy_options {
#define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_DISPLAYS 0x2
#define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERAS 0x4
#define SC_OPTION_LIST_CAMERA_SIZES 0x8 #define SC_OPTION_LIST_CAMERA_SIZES 0x8
#define SC_OPTION_LIST_APPS 0x10
uint8_t list; uint8_t list;
bool window; bool window;
bool mouse_hover; bool mouse_hover;

View File

@ -371,6 +371,9 @@ execute_server(struct sc_server *server,
if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { if (params->list & SC_OPTION_LIST_CAMERA_SIZES) {
ADD_PARAM("list_camera_sizes=true"); ADD_PARAM("list_camera_sizes=true");
} }
if (params->list & SC_OPTION_LIST_APPS) {
ADD_PARAM("list_apps=true");
}
#undef ADD_PARAM #undef ADD_PARAM

View File

@ -61,6 +61,7 @@ public class Options {
private boolean listDisplays; private boolean listDisplays;
private boolean listCameras; private boolean listCameras;
private boolean listCameraSizes; private boolean listCameraSizes;
private boolean listApps;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly // Options not used by the scrcpy client, but useful to use scrcpy-server directly
private boolean sendDeviceMeta = true; // send device name and size private boolean sendDeviceMeta = true; // send device name and size
@ -213,7 +214,7 @@ public class Options {
} }
public boolean getList() { public boolean getList() {
return listEncoders || listDisplays || listCameras || listCameraSizes; return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
} }
public boolean getListEncoders() { public boolean getListEncoders() {
@ -232,6 +233,10 @@ public class Options {
return listCameraSizes; return listCameraSizes;
} }
public boolean getListApps() {
return listApps;
}
public boolean getSendDeviceMeta() { public boolean getSendDeviceMeta() {
return sendDeviceMeta; return sendDeviceMeta;
} }
@ -395,6 +400,9 @@ public class Options {
case "list_camera_sizes": case "list_camera_sizes":
options.listCameraSizes = Boolean.parseBoolean(value); options.listCameraSizes = Boolean.parseBoolean(value);
break; break;
case "list_apps":
options.listApps = Boolean.parseBoolean(value);
break;
case "camera_id": case "camera_id":
if (!value.isEmpty()) { if (!value.isEmpty()) {
options.cameraId = value; options.cameraId = value;

View File

@ -292,6 +292,11 @@ public final class Server {
Workarounds.apply(); Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
} }
if (options.getListApps()) {
Workarounds.apply();
Ln.i("Processing Android apps... (this may take some time)");
Ln.i(LogUtils.buildAppListMessage());
}
// Just print the requested data, do not mirror // Just print the requested data, do not mirror
return; return;
} }

View File

@ -1,6 +1,7 @@
package com.genymobile.scrcpy.device; package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.DisplayControl;
@ -9,6 +10,10 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager; import com.genymobile.scrcpy.wrappers.WindowManager;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
@ -17,6 +22,9 @@ import android.view.InputEvent;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
public final class Device { public final class Device {
public static final int DISPLAY_ID_NONE = -1; public static final int DISPLAY_ID_NONE = -1;
@ -202,4 +210,37 @@ public final class Device {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
return displayInfo.getRotation(); return displayInfo.getRotation();
} }
public static List<DeviceApp> listApps() {
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));
}
return apps;
}
@SuppressLint("QueryPermissionsNeeded")
private static List<ApplicationInfo> getLaunchableApps(PackageManager pm) {
List<ApplicationInfo> result = new ArrayList<>();
for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) {
if (appInfo.enabled && getLaunchIntent(pm, appInfo.packageName) != null) {
result.add(appInfo);
}
}
return result;
}
public static Intent getLaunchIntent(PackageManager pm, String packageName) {
Intent launchIntent = pm.getLaunchIntentForPackage(packageName);
if (launchIntent != null) {
return launchIntent;
}
return pm.getLeanbackLaunchIntentForPackage(packageName);
}
} }

View File

@ -0,0 +1,26 @@
package com.genymobile.scrcpy.device;
public final class DeviceApp {
private final String packageName;
private final String name;
private final boolean system;
public DeviceApp(String packageName, String name, boolean system) {
this.packageName = packageName;
this.name = name;
this.system = system;
}
public String getPackageName() {
return packageName;
}
public String getName() {
return name;
}
public boolean isSystem() {
return system;
}
}

View File

@ -1,10 +1,13 @@
package com.genymobile.scrcpy.util; package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraCharacteristics;
@ -13,7 +16,9 @@ import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.util.Range; import android.util.Range;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@ -154,4 +159,53 @@ public final class LogUtils {
} }
return set; return set;
} }
@SuppressLint("QueryPermissionsNeeded")
public static String buildAppListMessage() {
StringBuilder builder = new StringBuilder("List of apps:");
List<DeviceApp> apps = Device.listApps();
// Sort by:
// 1. system flag (system apps are before non-system apps)
// 2. name
// 3. package name
// Comparator.comparing() was introduced in API 24, so it cannot be used here to simplify the code
Collections.sort(apps, (thisApp, otherApp) -> {
// System apps first
int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem());
if (cmp != 0) {
return cmp;
}
cmp = Objects.compare(thisApp.getName(), otherApp.getName(), String::compareTo);
if (cmp != 0) {
return cmp;
}
return Objects.compare(thisApp.getPackageName(), otherApp.getPackageName(), String::compareTo);
});
final int column = 30;
for (DeviceApp app : apps) {
String name = app.getName();
int padding = column - name.length();
builder.append("\n ");
if (app.isSystem()) {
builder.append("* ");
} else {
builder.append("- ");
}
builder.append(name);
if (padding > 0) {
builder.append(String.format("%" + padding + "s", " "));
} else {
builder.append("\n ").append(String.format("%" + column + "s", " "));
}
builder.append(" [").append(app.getPackageName()).append(']');
}
return builder.toString();
}
} }