Rework --start-app and --list-apps

This commit is contained in:
4nric 2024-11-30 22:37:26 +08:00
parent d01373c03c
commit 9d69d7eeb7
5 changed files with 293 additions and 182 deletions

View file

@ -5,12 +5,10 @@ import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
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.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.VirtualDisplayListener;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
@ -27,7 +25,6 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -610,37 +607,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
private void startApp(String name) {
boolean forceStopBeforeStart = name.startsWith("+");
if (forceStopBeforeStart) {
name = name.substring(1);
}
DeviceApp app;
boolean searchByName = name.startsWith("?");
if (searchByName) {
name = name.substring(1);
Ln.i("Processing Android apps... (this may take some time)");
List<DeviceApp> apps = Device.findByName(name);
if (apps.isEmpty()) {
Ln.w("No app found for name \"" + name + "\"");
return;
}
if (apps.size() > 1) {
String title = "No unique app found for name \"" + name + "\":";
Ln.w(LogUtils.buildAppListMessage(title, apps));
return;
}
app = apps.get(0);
} else {
app = Device.findByPackageName(name);
if (app == null) {
Ln.w("No app found for package \"" + name + "\"");
return;
}
}
Intent launchIntent = new Intent();
int startAppDisplayId = getStartAppDisplayId();
if (startAppDisplayId == Device.DISPLAY_ID_NONE) {
@ -648,8 +615,55 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return;
}
Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "...");
Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart);
if (name.contains("+") && name.contains("-")){
Ln.e("Can't make a (+) new instance if (-) force stop is also specified.");
return;
}
boolean newInstance = name.startsWith("+");
if (newInstance) {
name = name.substring(1);
}
boolean forceStopBeforeStart = name.startsWith("-");
if (forceStopBeforeStart) {
name = name.substring(1);
}
if (name.contains("/")){
launchIntent = Device.getIntentFromClassName(name.split("/")[0], name.split("/")[1]);
if (launchIntent == null) {
return;
}
} else {
boolean searchByName = name.startsWith("?") || name.contains(" ");
if (searchByName) {
if (name.contains("?")) {
name = name.substring(1);
}
launchIntent = Device.getIntentFromAppDrawer(name,false);
if (launchIntent == null){
return;
}
} else {
launchIntent = Device.getIntentFromAppDrawer(name,true);
if (launchIntent == null) {
return;
}
}
}
String packageName = launchIntent.getComponent().getPackageName();
String label = launchIntent.getStringExtra("APP_LABEL");
launchIntent.removeExtra("APP_LABEL");
Ln.i("Starting app \"" + label + "\" [" + packageName + "] on display " + startAppDisplayId + "...");
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (newInstance) {
launchIntent.addFlags(Intent. FLAG_ACTIVITY_MULTIPLE_TASK);
}
Device.startApp(launchIntent, startAppDisplayId, forceStopBeforeStart);
}
private int getStartAppDisplayId() {

View file

@ -2,6 +2,7 @@ package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.AppListProcessor;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ActivityManager;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
@ -12,10 +13,15 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.annotation.SuppressLint;
import android.app.UiModeManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.app.ActivityOptions;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@ -25,7 +31,6 @@ import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@ -221,84 +226,7 @@ public final class Device {
return displayInfo.getRotation();
}
public static List<DeviceApp> listApps() {
List<DeviceApp> apps = new ArrayList<>();
PackageManager pm = FakeContext.get().getPackageManager();
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
apps.add(toApp(pm, appInfo));
}
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);
}
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;
}
@SuppressLint("QueryPermissionsNeeded")
public static List<DeviceApp> findByName(String searchName) {
List<DeviceApp> result = new ArrayList<>();
searchName = searchName.toLowerCase(Locale.getDefault());
PackageManager pm = FakeContext.get().getPackageManager();
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
String name = pm.getApplicationLabel(appInfo).toString();
if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) {
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
result.add(new DeviceApp(appInfo.packageName, name, system));
}
}
return result;
}
public static void startApp(String packageName, int displayId, boolean forceStop) {
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);
public static void startApp(Intent launchIntent, int displayId, boolean forceStop) {
Bundle options = null;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_26_ANDROID_8_0) {
ActivityOptions launchOptions = ActivityOptions.makeBasic();
@ -308,8 +236,131 @@ public final class Device {
ActivityManager am = ServiceManager.getActivityManager();
if (forceStop) {
am.forceStopPackage(packageName);
am.forceStopPackage(launchIntent.getComponent().getPackageName());
}
am.startActivity(launchIntent, options);
}
@SuppressLint("QueryPermissionsNeeded")
public static List<ResolveInfo> getDrawerApps() {
Context context = FakeContext.get();
PackageManager pm = context.getPackageManager();
Intent intent = new Intent(Intent.ACTION_MAIN, null);
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
intent.addCategory(uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION?
Intent.CATEGORY_LEANBACK_LAUNCHER : Intent.CATEGORY_LAUNCHER);
return pm.queryIntentActivities(intent, 0);
}
public static Intent getIntentFromAppDrawer(String query, boolean isPackageName){
AppListProcessor appListProcessor = new AppListProcessor(isPackageName, true, query);
query = query.toLowerCase(Locale.getDefault());
Context context = FakeContext.get();
PackageManager pm = context.getPackageManager();
for (ResolveInfo drawerApp : getDrawerApps()) {
String packageName = drawerApp.activityInfo.packageName;
String label = drawerApp.loadLabel(pm).toString().toLowerCase(Locale.getDefault());
if (isPackageName){
if (packageName.equals(query)) {
ComponentName componentName = new ComponentName(packageName, drawerApp.activityInfo.name);
return new Intent().setComponent(componentName)
.putExtra("APP_LABEL", drawerApp.loadLabel(pm).toString());
} else if (packageName.contains(query)){
appListProcessor.addPotentialMatchesPkgName(drawerApp);
}
} else {
if (label.equals(query)) {
appListProcessor.addExactMatchesLabel(drawerApp);
} else if (label.contains(query)){
appListProcessor.addPotentialMatchesAppName(drawerApp);
}
}
}
//Suggestions will show regardless if MULTIPLE_EXACT_LABELS
Intent launchIntent = appListProcessor.getIntent(pm,false);
if (launchIntent == null){
Ln.w("Trying to find from list of all apps");
return getIntentFromListOfAllApps(appListProcessor.getOrgQuery(), isPackageName);
}
else if (launchIntent.getBooleanExtra("MULTIPLE_EXACT_LABELS", false)) {
//Let processResolvedLists() return a "garbage" intent if there are multiple exact labels.
// We want to avoid redundant check. This happens if first check "launchIntent == null"
// becomes true since getIntentFromListOfAllApps() also calls processResolvedLists().
// This limitation can be removed to instead list possible exact label matches
// from list of all apps also.
return null;
}
return launchIntent;
}
@SuppressLint("QueryPermissionsNeeded")
public static Intent getIntentFromListOfAllApps(String query, boolean isPackageName){
AppListProcessor appListProcessor = new AppListProcessor(isPackageName, false, query);
query = query.toLowerCase(Locale.getDefault());
Context context = FakeContext.get();
PackageManager pm = context.getPackageManager();
boolean isTV = false;
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
isTV = true;
}
for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) {
String packageName = appInfo.packageName;
String label = appInfo.loadLabel(pm).toString().toLowerCase(Locale.getDefault());
Intent launchIntent = isTV ?
pm.getLeanbackLaunchIntentForPackage(packageName) :
pm.getLaunchIntentForPackage(packageName);
if (isPackageName){
if (packageName.equals(query) && launchIntent == null) {
Ln.e("No launch intent for " + appInfo.loadLabel(pm) + " ["+packageName+"]");
return null;
} else if (packageName.equals(query)) {
return launchIntent.putExtra("APP_LABEL", label);
}else if (packageName.contains(query) && launchIntent != null){
appListProcessor.addPotentialMatchesPkgName(pm.resolveActivity(launchIntent, 0));
}
} else {
if (launchIntent == null) {
if (label.equals(query)){
Ln.w("Ignoring "+ appInfo.loadLabel(pm) + " ["+packageName+"] which has no launch intent");
}
continue;
}
ResolveInfo resolveInfo = pm.resolveActivity(launchIntent, 0);
if (label.equals(query)) {
appListProcessor.addExactMatchesLabel(resolveInfo);
} else if (label.contains(query)){
appListProcessor.addPotentialMatchesAppName(resolveInfo);
}
}
}
return appListProcessor.getIntent(pm,true);
}
@SuppressLint("QueryPermissionsNeeded")
public static Intent getIntentFromClassName(String packageName, String activityName){
Context context = FakeContext.get();
PackageManager pm = context.getPackageManager();
Intent intent = new Intent();
intent.setClassName(packageName, activityName);
List<ResolveInfo> list = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() == 1){
intent.putExtra("APP_LABEL", list.get(0).loadLabel(pm).toString());
Ln.i("Activity class {"+packageName+"/"+activityName+"} found");
return intent;
} else {
Ln.e("Activity class {"+packageName+"/"+activityName+"} does not exist");
return null;
}
}
}

View file

@ -1,26 +0,0 @@
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

@ -0,0 +1,80 @@
package com.genymobile.scrcpy.util;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import java.util.ArrayList;
import java.util.List;
public class AppListProcessor {
private final List<ResolveInfo> exactMatchesLabel = new ArrayList<>();
private final List<ResolveInfo> potentialMatchesAppName = new ArrayList<>();
private final List<ResolveInfo> potentialMatchesPkgName = new ArrayList<>();
private final boolean isPackageName; //Treated as label if false
private final String errorMessage;
private final String query;
public AppListProcessor(boolean isPackageName, boolean appDrawer, String orgQuery) {
this.isPackageName = isPackageName;
this.query = orgQuery;
String tmpErrMsg = isPackageName ? "No launchable app with package name " : "No unique launchable app named ";
this.errorMessage = tmpErrMsg + "\"" + orgQuery + "\" found from " + ( appDrawer ? "app drawer" : "list of all apps");
}
public String getOrgQuery(){
return query;
}
public void addExactMatchesLabel(ResolveInfo resolveInfo) {
exactMatchesLabel.add(resolveInfo);
}
public void addPotentialMatchesAppName(ResolveInfo resolveInfo) {
potentialMatchesAppName.add(resolveInfo);
}
public void addPotentialMatchesPkgName(ResolveInfo resolveInfo) {
potentialMatchesPkgName.add(resolveInfo);
}
public Intent getIntent(PackageManager pm, boolean showSuggestions){
String suggestions = "\n";
boolean multipleExactLabelMatches = false;
if (isPackageName){
if (!potentialMatchesPkgName.isEmpty()){
suggestions+=LogUtils.buildAppListMessage("Found "+potentialMatchesPkgName.size()+" potential matches:",potentialMatchesPkgName);
}
} else {
if (exactMatchesLabel.size() == 1){
ActivityInfo activityInfo = exactMatchesLabel.get(0).activityInfo;
ComponentName componentName = new ComponentName(activityInfo.packageName, activityInfo.name);
return new Intent().setComponent(componentName)
.putExtra("APP_LABEL", exactMatchesLabel.get(0).loadLabel(pm).toString());
} else{
if (!exactMatchesLabel.isEmpty()){
multipleExactLabelMatches = true;
showSuggestions = true;
suggestions+=LogUtils.buildAppListMessage("Found "+exactMatchesLabel.size()+" exact matches:",exactMatchesLabel)+"\n";
}
if (!potentialMatchesAppName.isEmpty()){
suggestions+=LogUtils.buildAppListMessage("Found " + potentialMatchesAppName.size() + " other potential " + (potentialMatchesAppName.size() == 1 ? "match:" : "matches:"), potentialMatchesAppName)+"\n";
}
}
}
if (showSuggestions){
Ln.e(errorMessage + (suggestions.equals("\n")? "\0" : suggestions));
} else {
Ln.e(errorMessage);
}
if (multipleExactLabelMatches){
return new Intent().putExtra("MULTIPLE_EXACT_LABELS", true);
} else {
return null;
}
}
}

View file

@ -1,17 +1,18 @@
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.audio.AudioCodec;
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.video.VideoCodec;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
@ -23,10 +24,12 @@ import android.media.MediaCodecList;
import android.os.Build;
import android.util.Range;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class LogUtils {
@ -191,54 +194,43 @@ public final class LogUtils {
public static String buildAppListMessage() {
List<DeviceApp> apps = Device.listApps();
return buildAppListMessage("List of apps:", apps);
List<ResolveInfo> drawerApps = Device.getDrawerApps();
return buildAppListMessage("List of apps:", drawerApps);
}
@SuppressLint("QueryPermissionsNeeded")
public static String buildAppListMessage(String title, List<DeviceApp> apps) {
StringBuilder builder = new StringBuilder(title);
public static String buildAppListMessage(String title, List<ResolveInfo> drawerApps) {
Map<String, String> appMap = new HashMap<>();
Context context = FakeContext.get();
// 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;
for (ResolveInfo app : drawerApps) {
appMap.put(app.activityInfo.packageName, app.loadLabel(context.getPackageManager()).toString());
}
TreeMap<String, String> sortedMap = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String key1, String key2) {
int labelComparison = appMap.get(key1).compareToIgnoreCase(appMap.get(key2));
if (labelComparison != 0) {
return labelComparison;
}
return key1.compareTo(key2); // Compare by package name if labels are the same
}
cmp = Objects.compare(thisApp.getName(), otherApp.getName(), String::compareTo);
if (cmp != 0) {
return cmp;
}
return Objects.compare(thisApp.getPackageName(), otherApp.getPackageName(), String::compareTo);
});
sortedMap.putAll(appMap);
StringBuilder builder = new StringBuilder(title);
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);
for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
int padding = column - entry.getValue().length();
builder.append("\n - ");
builder.append(entry.getValue());
if (padding > 0) {
builder.append(String.format("%" + padding + "s", " "));
} else {
builder.append("\n ").append(String.format("%" + column + "s", " "));
builder.append("\n - ").append(String.format("%" + column + "s", " "));
}
builder.append(" ").append(app.getPackageName());
builder.append(" ").append(entry.getKey());
}
return builder.toString();
}
}