From 6ff3d9b571e4d17f7876e8f35d5180bc54811dad Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 17:16:08 +0200 Subject: [PATCH] Add --list-apps Add an option to list all apps installed on the device: scrcpy --list-apps --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++ app/src/cli.c | 9 +++ app/src/options.h | 1 + app/src/server.c | 3 + .../java/com/genymobile/scrcpy/Options.java | 10 +++- .../java/com/genymobile/scrcpy/Server.java | 5 ++ .../com/genymobile/scrcpy/device/Device.java | 41 ++++++++++++++ .../genymobile/scrcpy/device/DeviceApp.java | 32 +++++++++++ .../com/genymobile/scrcpy/util/LogUtils.java | 56 +++++++++++++++++++ 11 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 4f40d466..f37da13a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,6 +33,7 @@ _scrcpy() { --keyboard= --kill-adb-on-close --legacy-paste + --list-apps --list-camera-sizes --list-cameras --list-displays diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index f65430e0..3f25b88d 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -40,6 +40,7 @@ arguments=( '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--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-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 7004c6dd..252ecc54 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -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. +.TP +.B \-\-list\-apps +List Android apps installed on the device. + .TP .B \-\-list\-camera\-sizes List the valid camera capture sizes. diff --git a/app/src/cli.c b/app/src/cli.c index ddf581f4..89b1fbd2 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -103,6 +103,7 @@ enum { OPT_AUDIO_DUP, OPT_GAMEPAD, OPT_NEW_DISPLAY, + OPT_LIST_APPS, }; struct sc_option { @@ -443,6 +444,11 @@ static const struct sc_option options[] = { "This is a workaround for some devices not behaving as " "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 = "list-cameras", @@ -2610,6 +2616,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_CAMERA_SIZES: opts->list |= SC_OPTION_LIST_CAMERA_SIZES; break; + case OPT_LIST_APPS: + opts->list |= SC_OPTION_LIST_APPS; + break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; diff --git a/app/src/options.h b/app/src/options.h index f3d27a88..7cbe2e5b 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -304,6 +304,7 @@ struct scrcpy_options { #define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERA_SIZES 0x8 +#define SC_OPTION_LIST_APPS 0x10 uint8_t list; bool window; bool mouse_hover; diff --git a/app/src/server.c b/app/src/server.c index 26725fa0..167582e4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -371,6 +371,9 @@ execute_server(struct sc_server *server, if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { ADD_PARAM("list_camera_sizes=true"); } + if (params->list & SC_OPTION_LIST_APPS) { + ADD_PARAM("list_apps=true"); + } #undef ADD_PARAM diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 201714e4..2881b026 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -61,6 +61,7 @@ public class Options { private boolean listDisplays; private boolean listCameras; private boolean listCameraSizes; + private boolean listApps; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -213,7 +214,7 @@ public class Options { } public boolean getList() { - return listEncoders || listDisplays || listCameras || listCameraSizes; + return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } public boolean getListEncoders() { @@ -232,6 +233,10 @@ public class Options { return listCameraSizes; } + public boolean getListApps() { + return listApps; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -395,6 +400,9 @@ public class Options { case "list_camera_sizes": options.listCameraSizes = Boolean.parseBoolean(value); break; + case "list_apps": + options.listApps = Boolean.parseBoolean(value); + break; case "camera_id": if (!value.isEmpty()) { options.cameraId = value; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fd854e06..4fc3f0fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -287,6 +287,11 @@ public final class Server { Workarounds.apply(); 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 return; } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 9ce57a79..f619f1e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,6 +1,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.ClipboardManager; 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.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.IBinder; import android.os.SystemClock; @@ -17,6 +22,9 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import java.util.ArrayList; +import java.util.List; + public final class Device { public static final int DISPLAY_ID_NONE = -1; @@ -201,4 +209,37 @@ public final class Device { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); return displayInfo.getRotation(); } + + public static List listApps() { + List 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, appInfo.enabled)); + } + + return apps; + } + + @SuppressLint("QueryPermissionsNeeded") + private static List getLaunchableApps(PackageManager pm) { + List result = new ArrayList<>(); + for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { + if (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); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java new file mode 100644 index 00000000..0ae547ff --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java @@ -0,0 +1,32 @@ +package com.genymobile.scrcpy.device; + +public final class DeviceApp { + + private final String packageName; + private final String name; + private final boolean system; + private final boolean enabled; + + public DeviceApp(String packageName, String name, boolean system, boolean enabled) { + this.packageName = packageName; + this.name = name; + this.system = system; + this.enabled = enabled; + } + + public String getPackageName() { + return packageName; + } + + public String getName() { + return name; + } + + public boolean isSystem() { + return system; + } + + public boolean isEnabled() { + return enabled; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 45ab4eba..b15c1db6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,10 +1,13 @@ 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.Size; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.annotation.SuppressLint; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; @@ -13,7 +16,9 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; import android.util.Range; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -154,4 +159,55 @@ public final class LogUtils { } return set; } + + @SuppressLint("QueryPermissionsNeeded") + public static String buildAppListMessage() { + StringBuilder builder = new StringBuilder("List of apps:"); + + List apps = Device.listApps(); + + // Sort by: + // 1. system flag (system apps are before non-system apps) + // 2. name + // 3. package name + 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(']'); + if (!app.isEnabled()) { + builder.append(" (disabled)"); + } + } + + return builder.toString(); + } }