diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5e64a4c5..7870235a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -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; @@ -28,7 +26,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; @@ -627,37 +624,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 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) { @@ -665,8 +632,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() { 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 3553dc27..aaaa4e11 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -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 listApps() { - List 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 getLaunchableApps(PackageManager pm) { - List 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 findByName(String searchName) { - List 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 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 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; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java deleted file mode 100644 index ed292efa..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java +++ /dev/null @@ -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; - } -} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/AppListProcessor.java b/server/src/main/java/com/genymobile/scrcpy/util/AppListProcessor.java new file mode 100644 index 00000000..664e0348 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/util/AppListProcessor.java @@ -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 exactMatchesLabel = new ArrayList<>(); + private final List potentialMatchesAppName = new ArrayList<>(); + private final List 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; + } + } +} 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 4f8927ec..c60eeabe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -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 { @@ -215,54 +218,43 @@ public final class LogUtils { public static String buildAppListMessage() { - List apps = Device.listApps(); - return buildAppListMessage("List of apps:", apps); + List drawerApps = Device.getDrawerApps(); + return buildAppListMessage("List of apps:", drawerApps); } - @SuppressLint("QueryPermissionsNeeded") - public static String buildAppListMessage(String title, List apps) { - StringBuilder builder = new StringBuilder(title); + public static String buildAppListMessage(String title, List drawerApps) { + Map 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 sortedMap = new TreeMap<>(new Comparator() { + @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 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(); } }