From e2220ccc7cc3eaece8cd92efbc95a9a63e7ec370 Mon Sep 17 00:00:00 2001 From: Roberto Viola Date: Wed, 3 Jul 2024 18:45:24 +0200 Subject: [PATCH] it doesn't work because now i'm sending stay awake that does work only with usb plugged in --- app/src/main/assets/scrcpy-server.jar | Bin 18751 -> 27383 bytes .../org/cagnulein/android_remote/CleanUp.java | 150 ++++++++ .../org/cagnulein/android_remote/Command.java | 43 +++ .../cagnulein/android_remote/FakeContext.java | 53 +++ .../java/org/cagnulein/android_remote/IO.java | 58 +++ .../org/cagnulein/android_remote/Server.java | 32 ++ .../cagnulein/android_remote/Settings.java | 82 +++++ .../android_remote/SettingsException.java | 11 + .../cagnulein/android_remote/Workarounds.java | 340 ++++++++++++++++++ .../wrappers/ActivityManager.java | 159 ++++++++ .../wrappers/ContentProvider.java | 161 +++++++++ .../wrappers/ServiceManager.java | 13 +- 12 files changed, 1100 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/org/cagnulein/android_remote/CleanUp.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/Command.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/FakeContext.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/IO.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/Settings.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/SettingsException.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/Workarounds.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/wrappers/ActivityManager.java create mode 100644 server/src/main/java/org/cagnulein/android_remote/wrappers/ContentProvider.java diff --git a/app/src/main/assets/scrcpy-server.jar b/app/src/main/assets/scrcpy-server.jar index 104abe393631fd2b60a267ac0478b890522baebf..b57524278814fba8c7b1aff7b7357ecd9c8caf86 100644 GIT binary patch delta 26621 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%0DRS_8p< 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 D50Tqt delta 17917 zcmV)bK&ij?)d9bf0g#!0)g6i)4@Cd~*pC1J3jhEBV{Bn_b7gZbWMz1@y$O6=RrLpc z?t8Oj&t#G`>2#YWDNX5;r7c~O(xge+rb(JKne2r&%e0v`nVC!`ZPT)py+Dfv*+B)R zh=K~Th>HGDP*Lko6p*SQAgHLQAgHMQ$^U!qy>Dicmc{?)|DX1MJLkT8&)v^G=iK|= zn+*E*rIapRPWN2ab^ovaI_HTq)5d4ix%VEpd-^xObN=}|a!!1VC`?5AI+nRff07Wl z&`|N`0W*_{ZUjwB1s`Y*I9CXwXLE^)aD8|V(ed3#$HnGzh}wa3ft|oG5CQfA7X#M< z4*-tD}md9`+;u(&jT+2uL5rXY1>ggPzkgG?Z8%`AMgVKAOu8#3E)EDGT;bsAMhOT zA0W4ns2ErXlmM%NH9$2`2h;0OX z0@{Eua2aqX@G$U0;5Fc%K;8gR8BhlVfm?ycfmeW&K<*&XT3`gY61WriF7OIq_Y<87 zYyw7r!@xbjmw+dM7l5~clpRC`z$%~}*ae&q90fiH{0Mjncne6{Nwf%P1_pu4ft!J^ z11|z^0gfSmqEcWz&%bep+rYno{{ZF~<{wA{TtE(x57Yn= z;Bw%9HsF5X7l1no*#laEbAesJ9^ew-I^cHTJHTIn+;Q{;*aqweZUG(vo&jD1-T>YP z{sScK0Y7jCPy(z3)&s4;Hed%Z0$d0j0lo^n2K*by*h@4WmbwC&32cp20z)irt zz}JAMfj5A^0Dl8gpfo)|0k8yE0c-#^0jfPKJ)z#-sz;8VcofiD4H1HKFV z9QZ465|}XoIR~nMUSKz{AGi{@1-K7*82AbBJHWXgvIQ&yYJqmZ4@7{=fFr;$;G4h? zfaieU0{;Lq4nUTHT3`Se2QCDT0-pdr2RsUV4|oRn4Pe56qyck)<-plMBhUd10ha-P zR|7Wyp8!4$90R@rd=K~`a02)Z@DCvKe3S(&0hR+bKnt)H*Z~BA2yi}d7`PeuJn(hk z2f)vPmw~r{lR(M^m|I{zunbrWYy>s~oxpZr2QUmof%AZm0k;8n0$&Ec4m<_?2>2!N zd*E%rb|Kax-~ko`D}b|sMqmpt1e^zdTn^j>+yUGRJP14n`~-L&cp3OTU>rnwfEQQ@ zYy^V99^gXYO5j%DKHvf1e}L}+&j2q0e*xYB%!{BOfLXv|pb-cImjkx}j{wJkmw-P4 z?*Mc$)+R6?r~uXj%|H(@3>*Ni2CfBe0lo-42z(8A8u%IT8t@0;ufV^7q)VWG695lT z1S|p801dzvU=WA^7X#M-Hvpdm?gkzJ9s`a8KL$4jl)r^#OIUt% zK>K45TY`8Irjqp35V}K`0ha?;0Ed7pfvbS4*+&D80M`I;@8~FSEpQ$1ao~F322y_B zO~B2-Ex@h7ZNTloCxA}^p91ax?gTyq+yxv1?gl;!+yi_L_&jhga3Ao01>lRomw@|$ z2Y@dF4+0MX4+DfxUQ>O*VU-Q zNeN+>4zEure`7)$IQ*=R-;_{qqYiVvCS5*Cw_H0e)?^a!Avksrs{M$8uEh$}R+p75cb@?1Wm=Hgh zP_JLNpTj#SF8@P+3F8w;2=7h^2X&a&d`O3Xqr=?h-|27~>U~{@c?^HA!@L&$pu;)f z`J)bd5q?95ixK{l4)Yp(M~ADn#={#Cep82A5dOOkZ%xR{>*CK=y@>y-4u=!UIS=7~ zSo{fjpYDnC98HLSvnL*AIebfpZ%*KOIf2KO(1v@!^A{a|z8~SYt#M0ePkuswpG+w8 z^@R2uPl$guA^bu@_}zpyypd4mYYBC|k--1X)_6NlBK)on+hAP(X7vT(f9f#n4J$9p zk2M#|65?65*)D{&oV=&QEGHyXm}Nld@Jv2y(BWc)O Gjy28-wKx|@T@#19?$b) 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 + * This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process). + */ +public final class CleanUp { + + private static final int MSG_TYPE_MASK = 0b11; + private static final int MSG_TYPE_RESTORE_STAY_ON = 0; + private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; + private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; + private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; + + private static final int MSG_PARAM_SHIFT = 2; + + private final OutputStream out; + + public CleanUp(OutputStream out) { + this.out = out; + } + + public static CleanUp configure(int displayId) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; + + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("CLASSPATH", Server.SERVER_PATH); + Process process = builder.start(); + return new CleanUp(process.getOutputStream()); + } + + private boolean sendMessage(int type, int param) { + assert (type & ~MSG_TYPE_MASK) == 0; + int msg = type | param << MSG_PARAM_SHIFT; + try { + out.write(msg); + out.flush(); + return true; + } catch (IOException e) { + Log.w("Cleanup", "Could not configure cleanup (type=" + type + ", param=" + param + ")", e); + return false; + } + } + + public boolean setRestoreStayOn(int restoreValue) { + // Restore the value (between 0 and 7), -1 to not restore + // + assert restoreValue >= -1 && restoreValue <= 7; + return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111); + } + + public boolean setDisableShowTouches(boolean disableOnExit) { + return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); + } + + public boolean setRestoreNormalPowerMode(boolean restoreOnExit) { + return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0); + } + + public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { + return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0); + } + + public static void unlinkSelf() { + try { + new File(Server.SERVER_PATH).delete(); + } catch (Exception e) { + Ln.e("Could not unlink server", e); + } + } + + public static void main(String... args) { + unlinkSelf(); + + int displayId = Integer.parseInt(args[0]); + + int restoreStayOn = -1; + boolean disableShowTouches = false; + boolean restoreNormalPowerMode = false; + boolean powerOffScreen = false; + + try { + // Wait for the server to die + int msg; + while ((msg = System.in.read()) != -1) { + int type = msg & MSG_TYPE_MASK; + int param = msg >> MSG_PARAM_SHIFT; + switch (type) { + case MSG_TYPE_RESTORE_STAY_ON: + restoreStayOn = param > 7 ? -1 : param; + break; + case MSG_TYPE_DISABLE_SHOW_TOUCHES: + disableShowTouches = param != 0; + break; + case MSG_TYPE_RESTORE_NORMAL_POWER_MODE: + restoreNormalPowerMode = param != 0; + break; + case MSG_TYPE_POWER_OFF_SCREEN: + powerOffScreen = param != 0; + break; + default: + Ln.w("Unexpected msg type: " + type); + break; + } + } + } catch (IOException e) { + // Expected when the server is dead + } + + Ln.i("Cleaning up"); + + if (disableShowTouches) { + Ln.i("Disabling \"show touches\""); + try { + Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + } catch (SettingsException e) { + Ln.e("Could not restore \"show_touches\"", e); + } + } + + if (restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + try { + Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + } catch (SettingsException e) { + Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); + } + } + + /* VIOLA + if (Device.isScreenOn()) { + if (powerOffScreen) { + Ln.i("Power off screen"); + Device.powerOffScreen(displayId); + } else if (restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } + }*/ + + System.exit(0); + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/Command.java b/server/src/main/java/org/cagnulein/android_remote/Command.java new file mode 100644 index 0000000..531670b --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/Command.java @@ -0,0 +1,43 @@ +package org.cagnulein.android_remote; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Scanner; + +public final class Command { + private Command() { + // not instantiable + } + + public static void exec(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + } + + public static String execReadLine(String... cmd) throws IOException, InterruptedException { + String result = null; + Process process = Runtime.getRuntime().exec(cmd); + Scanner scanner = new Scanner(process.getInputStream()); + if (scanner.hasNextLine()) { + result = scanner.nextLine(); + } + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return result; + } + + public static String execReadOutput(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + String output = IO.toString(process.getInputStream()); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return output; + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/FakeContext.java b/server/src/main/java/org/cagnulein/android_remote/FakeContext.java new file mode 100644 index 0000000..fd35fc4 --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/FakeContext.java @@ -0,0 +1,53 @@ +package org.cagnulein.android_remote; + +import android.annotation.TargetApi; +import android.content.AttributionSource; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.os.Process; + +public final class FakeContext extends ContextWrapper { + + public static final String PACKAGE_NAME = "com.android.shell"; + public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 + + private static final FakeContext INSTANCE = new FakeContext(); + + public static FakeContext get() { + return INSTANCE; + } + + private FakeContext() { + super(Workarounds.getSystemContext()); + } + + @Override + public String getPackageName() { + return PACKAGE_NAME; + } + + @Override + public String getOpPackageName() { + return PACKAGE_NAME; + } + + @TargetApi(Build.VERSION_CODES.S) + @Override + public AttributionSource getAttributionSource() { + AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); + builder.setPackageName(PACKAGE_NAME); + return builder.build(); + } + + // @Override to be added on SDK upgrade for Android 14 + @SuppressWarnings("unused") + public int getDeviceId() { + return 0; + } + + @Override + public Context getApplicationContext() { + return this; + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/IO.java b/server/src/main/java/org/cagnulein/android_remote/IO.java new file mode 100644 index 0000000..eeddc7a --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/IO.java @@ -0,0 +1,58 @@ +package org.cagnulein.android_remote; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import com.genymobile.scrcpy.BuildConfig; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Scanner; + +public final class IO { + private IO() { + // not instantiable + } + + public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { + // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so + // count the remaining bytes manually. + // See . + int remaining = from.remaining(); + while (remaining > 0) { + try { + int w = Os.write(fd, from); + if (BuildConfig.DEBUG && w < 0) { + // w should not be negative, since an exception is thrown on error + throw new AssertionError("Os.write() returned a negative value (" + w + ")"); + } + remaining -= w; + } catch (ErrnoException e) { + if (e.errno != OsConstants.EINTR) { + throw new IOException(e); + } + } + } + } + + public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { + writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); + } + + public static String toString(InputStream inputStream) { + StringBuilder builder = new StringBuilder(); + Scanner scanner = new Scanner(inputStream); + while (scanner.hasNextLine()) { + builder.append(scanner.nextLine()).append('\n'); + } + return builder.toString(); + } + + public static boolean isBrokenPipe(IOException e) { + Throwable cause = e.getCause(); + return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/Server.java b/server/src/main/java/org/cagnulein/android_remote/Server.java index b22887f..3fa15b5 100644 --- a/server/src/main/java/org/cagnulein/android_remote/Server.java +++ b/server/src/main/java/org/cagnulein/android_remote/Server.java @@ -1,17 +1,49 @@ package org.cagnulein.android_remote; +import android.os.BatteryManager; + +import java.io.File; import java.io.IOException; public final class Server { private static String ip = null; + public static final String SERVER_PATH; private Server() { // not instantiable } + static { + String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); + // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath + SERVER_PATH = classPaths[0]; + } + + private static void scrcpy(Options options) throws IOException { final Device device = new Device(options); + + /*if (options.getStayAwake())*/ { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + int restoreStayOn = Integer.parseInt(oldValue); + if (restoreStayOn != stayOn) { + // Restore only if the current value is different + if (!cleanUp.setRestoreStayOn(restoreStayOn)) { + Ln.e("Could not restore stay on on exit"); + } + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); + } + } + try (DroidConnection connection = DroidConnection.open(ip)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate()); diff --git a/server/src/main/java/org/cagnulein/android_remote/Settings.java b/server/src/main/java/org/cagnulein/android_remote/Settings.java new file mode 100644 index 0000000..f8c45a0 --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/Settings.java @@ -0,0 +1,82 @@ +package org.cagnulein.android_remote; + +import org.cagnulein.android_remote.wrappers.ContentProvider; +import org.cagnulein.android_remote.wrappers.ServiceManager; + +import android.os.Build; + +import java.io.IOException; + +public final class Settings { + + public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; + public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; + public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; + + private Settings() { + /* not instantiable */ + } + + private static void execSettingsPut(String table, String key, String value) throws SettingsException { + try { + Command.exec("settings", "put", table, key, value); + } catch (IOException | InterruptedException e) { + throw new SettingsException("put", table, key, value, e); + } + } + + private static String execSettingsGet(String table, String key) throws SettingsException { + try { + return Command.execReadLine("settings", "get", table, key); + } catch (IOException | InterruptedException e) { + throw new SettingsException("get", table, key, null, e); + } + } + + public static String getValue(String table, String key) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); + } catch (SettingsException e) { + Ln.w("Could not get settings value via ContentProvider, fallback to settings process" + e); + } + } + + return execSettingsGet(table, key); + } + + public static void putValue(String table, String key, String value) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); + } catch (SettingsException e) { + Ln.w("Could not put settings value via ContentProvider, fallback to settings process"+ e); + } + } + + execSettingsPut(table, key, value); + } + + public static String getAndPutValue(String table, String key, String value) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); + } + return oldValue; + } catch (SettingsException e) { + Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process"+ e); + } + } + + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/SettingsException.java b/server/src/main/java/org/cagnulein/android_remote/SettingsException.java new file mode 100644 index 0000000..b369c2b --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/SettingsException.java @@ -0,0 +1,11 @@ +package org.cagnulein.android_remote; + +public class SettingsException extends Exception { + private static String createMessage(String method, String table, String key, String value) { + return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : ""); + } + + public SettingsException(String method, String table, String key, String value, Throwable cause) { + super(createMessage(method, table, key, value), cause); + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/Workarounds.java b/server/src/main/java/org/cagnulein/android_remote/Workarounds.java new file mode 100644 index 0000000..18aec6b --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/Workarounds.java @@ -0,0 +1,340 @@ +package org.cagnulein.android_remote; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Application; +import android.content.AttributionSource; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ApplicationInfo; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.os.Build; +import android.os.Looper; +import android.os.Parcel; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") +public final class Workarounds { + + private static final Class ACTIVITY_THREAD_CLASS; + private static final Object ACTIVITY_THREAD; + + static { + prepareMainLooper(); + + try { + // ActivityThread activityThread = new ActivityThread(); + ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + ACTIVITY_THREAD = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private Workarounds() { + // not instantiable + } + + public static void apply(boolean audio, boolean camera) { + boolean mustFillConfigurationController = false; + boolean mustFillAppInfo = false; + boolean mustFillAppContext = false; + + if (Build.BRAND.equalsIgnoreCase("meizu")) { + // Workarounds must be applied for Meizu phones: + // - + // - + // - + // + // But only apply when strictly necessary, since workarounds can cause other issues: + // - + // - + mustFillAppInfo = true; + } else if (Build.BRAND.equalsIgnoreCase("honor")) { + // More workarounds must be applied for Honor devices: + // - + // + // The system context must not be set for all devices, because it would cause other problems: + // - + // - + mustFillAppInfo = true; + mustFillAppContext = true; + } + + if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + // Before Android 11, audio is not supported. + // Since Android 12, we can properly set a context on the AudioRecord. + // Only on Android 11 we must fill the application context for the AudioRecord to work. + mustFillAppContext = true; + } + + if (camera) { + mustFillAppInfo = true; + mustFillAppContext = true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), + // which requires a non-null ConfigurationController. + // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. + // + mustFillConfigurationController = true; + } + + if (mustFillConfigurationController) { + // Must be call before fillAppContext() because it is necessary to get a valid system context + fillConfigurationController(); + } + if (mustFillAppInfo) { + fillAppInfo(); + } + if (mustFillAppContext) { + fillAppContext(); + } + } + + @SuppressWarnings("deprecation") + private static void prepareMainLooper() { + // Some devices internally create a Handler when creating an input Surface, causing an exception: + // "Can't create handler inside thread that has not called Looper.prepare()" + // + // + // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: + // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' + // on a null object reference" + // + Looper.prepareMainLooper(); + } + + private static void fillAppInfo() { + try { + // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); + Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); + Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); + appBindDataConstructor.setAccessible(true); + Object appBindData = appBindDataConstructor.newInstance(); + + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = FakeContext.PACKAGE_NAME; + + // appBindData.appInfo = applicationInfo; + Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); + appInfoField.setAccessible(true); + appInfoField.set(appBindData, applicationInfo); + + // activityThread.mBoundApplication = appBindData; + Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); + mBoundApplicationField.setAccessible(true); + mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not fill app info: " + throwable.getMessage()); + } + } + + private static void fillAppContext() { + try { + Application app = new Application(); + Field baseField = ContextWrapper.class.getDeclaredField("mBase"); + baseField.setAccessible(true); + baseField.set(app, FakeContext.get()); + + // activityThread.mInitialApplication = app; + Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); + mInitialApplicationField.setAccessible(true); + mInitialApplicationField.set(ACTIVITY_THREAD, app); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not fill app context: " + throwable.getMessage()); + } + } + + private static void fillConfigurationController() { + try { + Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); + Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); + configurationControllerConstructor.setAccessible(true); + Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + + Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); + configurationControllerField.setAccessible(true); + configurationControllerField.set(ACTIVITY_THREAD, configurationController); + } catch (Throwable throwable) { + Ln.d("Could not fill configuration: " + throwable.getMessage()); + } + } + + static Context getSystemContext() { + try { + Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); + return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not get system context: " + throwable.getMessage()); + return null; + } + } + + @TargetApi(Build.VERSION_CODES.R) + @SuppressLint("WrongConstant,MissingPermission") + public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { + // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. + // + // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses + // reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do). + // As a result, the modified code was not executed. + try { + // AudioRecord audioRecord = new AudioRecord(0L); + Constructor audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class); + audioRecordConstructor.setAccessible(true); + AudioRecord audioRecord = audioRecordConstructor.newInstance(0L); + + // audioRecord.mRecordingState = RECORDSTATE_STOPPED; + Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState"); + mRecordingStateField.setAccessible(true); + mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED); + + Looper looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + + // audioRecord.mInitializationLooper = looper; + Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper"); + mInitializationLooperField.setAccessible(true); + mInitializationLooperField.set(audioRecord, looper); + + // Create `AudioAttributes` with fixed capture preset + int capturePreset = source; + AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder(); + Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class); + setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset); + AudioAttributes attributes = audioAttributesBuilder.build(); + + // audioRecord.mAudioAttributes = attributes; + Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes"); + mAudioAttributesField.setAccessible(true); + mAudioAttributesField.set(audioRecord, attributes); + + // audioRecord.audioParamCheck(capturePreset, sampleRate, encoding); + Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class); + audioParamCheckMethod.setAccessible(true); + audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding); + + // audioRecord.mChannelCount = channels + Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount"); + mChannelCountField.setAccessible(true); + mChannelCountField.set(audioRecord, channels); + + // audioRecord.mChannelMask = channelMask + Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask"); + mChannelMaskField.setAccessible(true); + mChannelMaskField.set(audioRecord, channelMask); + + int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); + int bufferSizeInBytes = minBufferSize * 8; + + // audioRecord.audioBuffSizeCheck(bufferSizeInBytes) + Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class); + audioBuffSizeCheckMethod.setAccessible(true); + audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes); + + final int channelIndexMask = 0; + + int[] sampleRateArray = new int[]{sampleRate}; + int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; + + int initResult; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // private native final int native_setup(Object audiorecord_this, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, String opPackageName, + // long nativeRecordInJavaObj); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, + int.class, int.class, int.class, int[].class, String.class, long.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, sampleRateArray, + channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(), + 0L); + } else { + // Assume `context` is never `null` + AttributionSource attributionSource = FakeContext.get().getAttributionSource(); + + // Assume `attributionSource.getPackageName()` is never null + + // ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState() + Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState"); + asScopedParcelStateMethod.setAccessible(true); + + try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) { + Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); + Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // private native int native_setup(Object audiorecordThis, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, + // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, + int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, + sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, + attributionSourceParcel, 0L, 0); + } else { + // Android 14 added a new int parameter "halInputFlags" + // + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, + int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, + sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, + attributionSourceParcel, 0L, 0, 0); + } + } + } + + if (initResult != AudioRecord.SUCCESS) { + Log.d("Workarounds", "Error code " + initResult + " when initializing native AudioRecord object."); + throw new RuntimeException("Cannot create AudioRecord"); + } + + // mSampleRate = sampleRate[0] + Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate"); + mSampleRateField.setAccessible(true); + mSampleRateField.set(audioRecord, sampleRateArray[0]); + + // audioRecord.mSessionId = session[0] + Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId"); + mSessionIdField.setAccessible(true); + mSessionIdField.set(audioRecord, session[0]); + + // audioRecord.mState = AudioRecord.STATE_INITIALIZED + Field mStateField = AudioRecord.class.getDeclaredField("mState"); + mStateField.setAccessible(true); + mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED); + + return audioRecord; + } catch (Exception e) { + Ln.e("Failed to invoke AudioRecord..", e); + throw new RuntimeException("Cannot create AudioRecord"); + } + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/wrappers/ActivityManager.java b/server/src/main/java/org/cagnulein/android_remote/wrappers/ActivityManager.java new file mode 100644 index 0000000..bf72bb0 --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/wrappers/ActivityManager.java @@ -0,0 +1,159 @@ +package org.cagnulein.android_remote.wrappers; + +import org.cagnulein.android_remote.FakeContext; +import org.cagnulein.android_remote.Ln; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IInterface; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +@SuppressLint("PrivateApi,DiscouragedPrivateApi") +public final class ActivityManager { + + private final IInterface manager; + private Method getContentProviderExternalMethod; + private boolean getContentProviderExternalMethodNewVersion = true; + private Method removeContentProviderExternalMethod; + private Method startActivityAsUserMethod; + private Method forceStopPackageMethod; + + static ActivityManager create() { + try { + // On old Android versions, the ActivityManager is not exposed via AIDL, + // so use ActivityManagerNative.getDefault() + Class cls = Class.forName("android.app.ActivityManagerNative"); + Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); + IInterface am = (IInterface) getDefaultMethod.invoke(null); + return new ActivityManager(am); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + ActivityManager(IInterface manager) { + this.manager = manager; + } + + private Method getGetContentProviderExternalMethod() throws NoSuchMethodException { + if (getContentProviderExternalMethod == null) { + try { + getContentProviderExternalMethod = manager.getClass() + .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class); + } catch (NoSuchMethodException e) { + // old version + getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); + getContentProviderExternalMethodNewVersion = false; + } + } + return getContentProviderExternalMethod; + } + + private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException { + if (removeContentProviderExternalMethod == null) { + removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class); + } + return removeContentProviderExternalMethod; + } + + @TargetApi(Build.VERSION_CODES.Q) + private ContentProvider getContentProviderExternal(String name, IBinder token) { + try { + Method method = getGetContentProviderExternalMethod(); + Object[] args; + if (getContentProviderExternalMethodNewVersion) { + // new version + args = new Object[]{name, FakeContext.ROOT_UID, token, null}; + } else { + // old version + args = new Object[]{name, FakeContext.ROOT_UID, token}; + } + // ContentProviderHolder providerHolder = getContentProviderExternal(...); + Object providerHolder = method.invoke(manager, args); + if (providerHolder == null) { + return null; + } + // IContentProvider provider = providerHolder.provider; + Field providerField = providerHolder.getClass().getDeclaredField("provider"); + providerField.setAccessible(true); + Object provider = providerField.get(providerHolder); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, name, token); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + void removeContentProviderExternal(String name, IBinder token) { + try { + Method method = getRemoveContentProviderExternalMethod(); + method.invoke(manager, name, token); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + } + } + + public ContentProvider createSettingsProvider() { + return getContentProviderExternal("settings", new Binder()); + } + + private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { + if (startActivityAsUserMethod == null) { + Class iApplicationThreadClass = Class.forName("android.app.IApplicationThread"); + Class profilerInfo = Class.forName("android.app.ProfilerInfo"); + startActivityAsUserMethod = manager.getClass() + .getMethod("startActivityAsUser", iApplicationThreadClass, String.class, Intent.class, String.class, IBinder.class, String.class, + int.class, int.class, profilerInfo, Bundle.class, int.class); + } + return startActivityAsUserMethod; + } + + @SuppressWarnings("ConstantConditions") + public int startActivity(Intent intent) { + try { + Method method = getStartActivityAsUserMethod(); + return (int) method.invoke( + /* this */ manager, + /* caller */ null, + /* callingPackage */ FakeContext.PACKAGE_NAME, + /* intent */ intent, + /* resolvedType */ null, + /* resultTo */ null, + /* resultWho */ null, + /* requestCode */ 0, + /* startFlags */ 0, + /* profilerInfo */ null, + /* bOptions */ null, + /* userId */ /* UserHandle.USER_CURRENT */ -2); + } catch (Throwable e) { + Ln.e("Could not invoke method", e); + return 0; + } + } + + private Method getForceStopPackageMethod() throws NoSuchMethodException { + if (forceStopPackageMethod == null) { + forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class); + } + return forceStopPackageMethod; + } + + public void forceStopPackage(String packageName) { + try { + Method method = getForceStopPackageMethod(); + method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2); + } catch (Throwable e) { + Ln.e("Could not invoke method", e); + } + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/wrappers/ContentProvider.java b/server/src/main/java/org/cagnulein/android_remote/wrappers/ContentProvider.java new file mode 100644 index 0000000..df36ce2 --- /dev/null +++ b/server/src/main/java/org/cagnulein/android_remote/wrappers/ContentProvider.java @@ -0,0 +1,161 @@ +package org.cagnulein.android_remote.wrappers; + +import org.cagnulein.android_remote.FakeContext; +import org.cagnulein.android_remote.Ln; +import org.cagnulein.android_remote.SettingsException; + +import android.annotation.SuppressLint; +import android.content.AttributionSource; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; + +import java.io.Closeable; +import java.lang.reflect.Method; + +public final class ContentProvider implements Closeable { + + public static final String TABLE_SYSTEM = "system"; + public static final String TABLE_SECURE = "secure"; + public static final String TABLE_GLOBAL = "global"; + + // See android/providerHolder/Settings.java + private static final String CALL_METHOD_GET_SYSTEM = "GET_system"; + private static final String CALL_METHOD_GET_SECURE = "GET_secure"; + private static final String CALL_METHOD_GET_GLOBAL = "GET_global"; + + private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; + private static final String CALL_METHOD_PUT_SECURE = "PUT_secure"; + private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global"; + + private static final String CALL_METHOD_USER_KEY = "_user"; + + private static final String NAME_VALUE_TABLE_VALUE = "value"; + + private final ActivityManager manager; + // android.content.IContentProvider + private final Object provider; + private final String name; + private final IBinder token; + + private Method callMethod; + private int callMethodVersion; + + ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { + this.manager = manager; + this.provider = provider; + this.name = name; + this.token = token; + } + + @SuppressLint("PrivateApi") + private Method getCallMethod() throws NoSuchMethodException { + if (callMethod == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 0; + } else { + // old versions + try { + callMethod = provider.getClass() + .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 1; + } catch (NoSuchMethodException e1) { + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 2; + } catch (NoSuchMethodException e2) { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodVersion = 3; + } + } + } + } + return callMethod; + } + + private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException { + try { + Method method = getCallMethod(); + Object[] args; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) { + args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; + } else { + switch (callMethodVersion) { + case 1: + args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 2: + args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras}; + break; + default: + args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras}; + break; + } + } + return (Bundle) method.invoke(provider, args); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + throw e; + } + } + + public void close() { + manager.removeContentProviderExternal(name, token); + } + + private static String getGetMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_GET_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_GET_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_GET_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + private static String getPutMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_PUT_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_PUT_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_PUT_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + public String getValue(String table, String key) throws SettingsException { + String method = getGetMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID); + try { + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } catch (Exception e) { + throw new SettingsException(table, "get", key, null, e); + } + + } + + public void putValue(String table, String key, String value) throws SettingsException { + String method = getPutMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID); + arg.putString(NAME_VALUE_TABLE_VALUE, value); + try { + call(method, key, arg); + } catch (Exception e) { + throw new SettingsException(table, "put", key, value, e); + } + } +} diff --git a/server/src/main/java/org/cagnulein/android_remote/wrappers/ServiceManager.java b/server/src/main/java/org/cagnulein/android_remote/wrappers/ServiceManager.java index 2b21647..2049911 100644 --- a/server/src/main/java/org/cagnulein/android_remote/wrappers/ServiceManager.java +++ b/server/src/main/java/org/cagnulein/android_remote/wrappers/ServiceManager.java @@ -8,13 +8,15 @@ @SuppressLint("PrivateApi") public final class ServiceManager { - private final Method getServiceMethod; + private static Method getServiceMethod = null; private WindowManager windowManager; private DisplayManager displayManager; private InputManager inputManager; private PowerManager powerManager; + private static ActivityManager activityManager; + public ServiceManager() { try { getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); @@ -23,7 +25,7 @@ public ServiceManager() { } } - private IInterface getService(String service, String type) { + private static IInterface getService(String service, String type) { try { IBinder binder = (IBinder) getServiceMethod.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); @@ -60,4 +62,11 @@ public PowerManager getPowerManager() { } return powerManager; } + + public static ActivityManager getActivityManager() { + if (activityManager == null) { + activityManager = new ActivityManager(getService("activity", "android.app.IActivityManager")); + } + return activityManager; + } }