├── app ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ ├── com │ │ │ ├── android │ │ │ │ └── internal │ │ │ │ │ └── app │ │ │ │ │ └── IAppOpsService.java │ │ │ └── termux │ │ │ │ └── termuxam │ │ │ │ ├── reflection │ │ │ │ ├── result │ │ │ │ │ ├── MethodInvokeResult.java │ │ │ │ │ └── InvokeResult.java │ │ │ │ └── ReflectionUtils.java │ │ │ │ ├── logger │ │ │ │ └── Logger.java │ │ │ │ ├── FakeContext.java │ │ │ │ ├── BaseCommand.java │ │ │ │ ├── ShellCommand.java │ │ │ │ ├── PermissionUtils.java │ │ │ │ ├── ActivityManager.java │ │ │ │ ├── Workarounds.java │ │ │ │ ├── CrossVersionReflectedMethod.java │ │ │ │ ├── IActivityManager.java │ │ │ │ └── IntentCmd.java │ │ │ └── android │ │ │ ├── os │ │ │ └── ServiceManager.java │ │ │ └── content │ │ │ └── IIntentReceiver.java │ └── androidTest │ │ ├── aidl │ │ └── com │ │ │ └── termux │ │ │ └── termuxam │ │ │ └── ITestComponentsService.aidl │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── termux │ │ └── termuxam │ │ ├── test │ │ ├── TestActivity.java │ │ ├── TestReceiver.java │ │ ├── TestService.java │ │ └── TestComponentsService.java │ │ ├── IntentCmdTest.java │ │ └── IActivityManagerTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── am-apk-installed ├── gradle.properties ├── README.md ├── am-libexec-packaged ├── gradlew.bat ├── gradlew └── LICENSE /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/termux/TermuxAm/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /am-apk-installed: -------------------------------------------------------------------------------- 1 | #!/data/data/com.termux/files/usr/bin/sh 2 | export CLASSPATH="$(pm path com.termux.termuxam 2>&1 null if the service doesn't exist 10 | */ 11 | public static IBinder getService(String name) { 12 | throw new RuntimeException("STUB"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/reflection/result/MethodInvokeResult.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.reflection.result; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | /** 6 | * Result for an invocation of a {@link Method} that has a non-void return type. 7 | * 8 | * See also {@link InvokeResult}. 9 | */ 10 | public class MethodInvokeResult extends InvokeResult { 11 | 12 | public MethodInvokeResult(boolean success, Object value) { 13 | super(success, value); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/android/content/IIntentReceiver.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Bundle; 4 | 5 | /** 6 | * Stub - will be replaced by system at runtime 7 | */ 8 | public interface IIntentReceiver { 9 | public static abstract class Stub implements IIntentReceiver { 10 | public abstract void performReceive(Intent intent, int resultCode, String data, Bundle extras, 11 | boolean ordered, boolean sticky, int sendingUser); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/reflection/result/InvokeResult.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.reflection.result; 2 | 3 | /** 4 | * Result for an invocation. 5 | */ 6 | public class InvokeResult { 7 | 8 | /** Whether invocation was successful. */ 9 | public boolean success; 10 | /** The result {@link Object} for the invocation. */ 11 | public Object value; 12 | 13 | public InvokeResult(boolean success, Object value) { 14 | this.success = success; 15 | this.value = value; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/androidTest/aidl/com/termux/termuxam/ITestComponentsService.aidl: -------------------------------------------------------------------------------- 1 | // ITestComponentsService.aidl 2 | package com.termux.termuxam; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | interface ITestComponentsService { 7 | /** 8 | * Demonstrates some basic types that you can use as parameters 9 | * and return values in AIDL. 10 | */ 11 | /*void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, 12 | double aDouble, String aString);*/ 13 | void prepareAwait(); 14 | String await(); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/termux/termuxam/test/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.test; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | import androidx.annotation.Nullable; 7 | 8 | import com.termux.termuxam.IActivityManagerTest; 9 | 10 | /** 11 | * {@link Activity} used for {@link IActivityManagerTest#testStartActivity()} 12 | */ 13 | public class TestActivity extends Activity { 14 | @Override 15 | protected void onCreate(@Nullable Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | TestComponentsService.noteEvent("TestActivity " + getIntent().getAction()); 18 | finish(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/termux/termuxam/test/TestReceiver.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.test; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | import com.termux.termuxam.IActivityManagerTest; 8 | 9 | /** 10 | * {@link BroadcastReceiver} used in {@link IActivityManagerTest#testBroadcastIntent()} 11 | */ 12 | public class TestReceiver extends BroadcastReceiver { 13 | public static final String REPLY_DATA = "TestReceiver.REPLY_DATA:"; 14 | 15 | @Override 16 | public void onReceive(Context context, Intent intent) { 17 | setResultData(REPLY_DATA + intent.getAction()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | android.useAndroidX=true 19 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Oreo-compatible `am` command reimplementation 2 | `am` (Activity Manager) command in Android can be used to start Activity 3 | or send Broadcast from shell, however since Android Oreo that command 4 | only works from adb shell, not from apps. This is modified version of that 5 | command that is usable from app. 6 | 7 | # Running 8 | In this repository there are two wrapper scripts: 9 | * `am-libexec-packaged` 10 | * `am-apk-installed` 11 | 12 | First one is for use as installed package in Termux, while second one 13 | is for development, using TermuxAm apk that is installed as app in Android, 14 | allowing installation of Java part from Android Studio 15 | 16 | # Running tests/debugging 17 | Tests checking IActivityManager wrapper class are in `app/src/androidTest/java/com/termux/termuxam/IActivityManagerTest.java` 18 | and are runnable/debuggable from Android Studio 19 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/termux/termuxam/test/TestService.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.test; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.IBinder; 6 | 7 | import androidx.annotation.Nullable; 8 | 9 | import com.termux.termuxam.IActivityManagerTest; 10 | 11 | /** 12 | * {@link Service} used in {@link IActivityManagerTest#testStartStopService()} 13 | */ 14 | public class TestService extends Service { 15 | 16 | @Override 17 | public int onStartCommand(Intent intent, int flags, int startId) { 18 | TestComponentsService.noteEvent("Start TestService " + intent.getAction()); 19 | return START_NOT_STICKY; 20 | } 21 | 22 | @Override 23 | public void onDestroy() { 24 | TestComponentsService.noteEvent("Stop TestService"); 25 | super.onDestroy(); 26 | } 27 | 28 | @Nullable 29 | @Override 30 | public IBinder onBind(Intent intent) { 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/termux/termuxam/IntentCmdTest.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import android.content.Intent; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | import java.net.URISyntaxException; 9 | import java.util.ArrayList; 10 | 11 | public class IntentCmdTest { 12 | @Test 13 | public void testExtraStringArray() throws URISyntaxException { 14 | ShellCommand shellCommand = new ShellCommand(); 15 | shellCommand.init(new String[] { "--esa", "extra-name", "aaa,bbb\\,ccc,ddd" }, 0); 16 | 17 | Intent intent = IntentCmd.parseCommandArgs(shellCommand, null); 18 | String[] resultExtra = intent.getStringArrayExtra("extra-name"); 19 | Assert.assertArrayEquals(new String[] { "aaa", "bbb,ccc", "ddd" }, resultExtra); 20 | } 21 | 22 | @Test 23 | public void testExtraStringArrayList() throws URISyntaxException { 24 | ShellCommand shellCommand = new ShellCommand(); 25 | shellCommand.init(new String[] { "--esal", "extra-name", "aaa,bbb\\,ccc,ddd" }, 0); 26 | 27 | Intent intent = IntentCmd.parseCommandArgs(shellCommand, null); 28 | ArrayList resultExtra = intent.getStringArrayListExtra("extra-name"); 29 | Assert.assertArrayEquals(new Object[] { "aaa", "bbb,ccc", "ddd" }, resultExtra.toArray()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /am-libexec-packaged: -------------------------------------------------------------------------------- 1 | #!/data/data/com.termux/files/usr/bin/sh 2 | TERMUX_AM_VERSION=0.8.0 3 | AM_APK_PATH="@TERMUX_PREFIX@/libexec/termux-am/am.apk" 4 | 5 | if [ "$1" = "--version" ]; then 6 | echo "$TERMUX_AM_VERSION" 7 | exit 0 8 | fi 9 | 10 | # If apk file is writable and current effective user is not root (0), 11 | # system (1000) and shell (2000), then remove write bit from apk 12 | # permissions for current used for Android >= 14 since it will trigger 13 | # the `SecurityException: Writable dex file '/path/to/am.apk' is not allowed.` 14 | # exception in logcat by SystemClassLoader and cause a SIGABRT for the 15 | # app_process. 16 | # - https://github.com/termux/termux-packages/issues/16255 17 | # - https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading 18 | # - https://cs.android.com/android/_/android/platform/art/+/03ac3eb0fc36be97f301ac60e85e1bb7ca52fa12 19 | # - https://cs.android.com/android/_/android/platform/art/+/d3a8a9e960d533f39b6bafc785599eae838a6351 20 | # - https://cs.android.com/android/_/android/platform/art/+/03ac3eb0fc36be97f301ac60e85e1bb7ca52fa12:runtime/native/dalvik_system_DexFile.cc;l=335 21 | if [ -w "$AM_APK_PATH" ]; then 22 | user="$(id -u)" 23 | if [ "$user" != "0" ] && [ "$user" != "1000" ] && [ "$user" != "2000" ]; then 24 | chmod 0400 "$AM_APK_PATH" || exit $? 25 | fi 26 | fi 27 | 28 | export CLASSPATH="$AM_APK_PATH" 29 | unset LD_LIBRARY_PATH LD_PRELOAD 30 | exec /system/bin/app_process -Xnoimage-dex2oat / com.termux.termuxam.Am "$@" 31 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace "com.termux.termuxam" 5 | compileSdkVersion 33 6 | defaultConfig { 7 | applicationId "com.termux.termuxam" 8 | minSdkVersion 21 9 | // Note: targetSdkVersion affects only tests, 10 | // normally, even though this is packaged as apk, 11 | // it's not loaded as apk so targetSdkVersion is ignored. 12 | // targetSdkVersion this must be < 28 because this application accesses hidden apis 13 | //noinspection ExpiredTargetSdkVersion,OldTargetApi 14 | targetSdkVersion 27 15 | versionCode 1 16 | versionName "0.1" 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | 19 | buildConfigField "String", "TERMUX_PACKAGE_NAME", "\"com.termux\"" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | } 29 | 30 | dependencies { 31 | //implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | //testImplementation 'junit:junit:4.12' 33 | androidTestImplementation "androidx.test:runner:1.5.2" 34 | androidTestImplementation "androidx.test.ext:junit:1.1.5" 35 | 36 | implementation "androidx.annotation:annotation:1.7.1" 37 | 38 | // https://github.com/LSPosed/AndroidHiddenApiBypass | https://mvnrepository.com/artifact/org.lsposed.hiddenapibypass/hiddenapibypass 39 | implementation "org.lsposed.hiddenapibypass:hiddenapibypass:4.3" 40 | } 41 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/termux/termuxam/test/TestComponentsService.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.test; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.IBinder; 6 | import android.os.RemoteException; 7 | 8 | import androidx.annotation.Nullable; 9 | 10 | import com.termux.termuxam.ITestComponentsService; 11 | 12 | import java.util.concurrent.CountDownLatch; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * Helper service for reporting operations performed on test 17 | * {@link TestActivity} and {@link TestService} 18 | */ 19 | public class TestComponentsService extends Service { 20 | 21 | private static CountDownLatch awaitedEventLatch; 22 | private static String awaitedEvent; 23 | 24 | static void noteEvent(String name) { 25 | if (awaitedEventLatch != null) { 26 | awaitedEvent = name; 27 | awaitedEventLatch.countDown(); 28 | } 29 | } 30 | 31 | private final ITestComponentsService.Stub aidlImpl = new ITestComponentsService.Stub() { 32 | @Override 33 | public void prepareAwait() throws RemoteException { 34 | awaitedEvent = null; 35 | awaitedEventLatch = new CountDownLatch(1); 36 | } 37 | 38 | @Override 39 | public String await() throws RemoteException { 40 | try { 41 | if (awaitedEventLatch.await(5, TimeUnit.SECONDS)) { 42 | return awaitedEvent; 43 | } 44 | return "timed out"; 45 | } catch (InterruptedException e) { 46 | return "await interrupted"; 47 | } 48 | } 49 | }; 50 | 51 | @Nullable 52 | @Override 53 | public IBinder onBind(Intent intent) { 54 | return aidlImpl; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/logger/Logger.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.logger; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | import java.io.StringWriter; 6 | 7 | public class Logger { 8 | 9 | public static void logError(String message) { 10 | logError(null, message); 11 | } 12 | 13 | public static void logError(String tag, String message) { 14 | System.err.println((tag != null ? tag + " " : "") + message); 15 | } 16 | 17 | 18 | 19 | public static void logStackTraceWithMessage(String message, Throwable throwable) { 20 | logStackTraceWithMessage(null, message, throwable); 21 | } 22 | 23 | public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) { 24 | Logger.logError(tag, getMessageAndStackTraceString(message, throwable)); 25 | } 26 | 27 | public static String getMessageAndStackTraceString(String message, Throwable throwable) { 28 | if (message == null && throwable == null) 29 | return null; 30 | else if (message != null && throwable != null) 31 | return message + ":\n" + getStackTraceString(throwable); 32 | else if (throwable == null) 33 | return message; 34 | else 35 | return getStackTraceString(throwable); 36 | } 37 | 38 | public static String getStackTraceString(Throwable throwable) { 39 | if (throwable == null) return null; 40 | 41 | String stackTraceString = null; 42 | 43 | try { 44 | StringWriter errors = new StringWriter(); 45 | PrintWriter pw = new PrintWriter(errors); 46 | throwable.printStackTrace(pw); 47 | pw.close(); 48 | stackTraceString = errors.toString(); 49 | errors.close(); 50 | } catch (IOException e) { 51 | e.printStackTrace(); 52 | } 53 | 54 | return stackTraceString; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/FakeContext.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.AttributionSource; 5 | import android.content.MutableContextWrapper; 6 | import android.os.Build; 7 | import android.os.Process; 8 | 9 | /** 10 | * - https://github.com/Genymobile/scrcpy/blob/v2.1.1/server/src/main/java/com/genymobile/scrcpy/FakeContext.java 11 | */ 12 | public class FakeContext extends MutableContextWrapper { 13 | 14 | private static final String TERMUX_PACKAGES_BUILD_PACKAGE_NAME = "@TERMUX_APP_PACKAGE@"; 15 | public static String PACKAGE_NAME = setPackageName(); 16 | 17 | public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 18 | 19 | private static final FakeContext INSTANCE = new FakeContext(); 20 | 21 | public static FakeContext get() { 22 | return INSTANCE; 23 | } 24 | 25 | private FakeContext() { 26 | super(null); 27 | } 28 | 29 | @Override 30 | public String getPackageName() { 31 | return PACKAGE_NAME; 32 | } 33 | 34 | @SuppressWarnings("ConstantConditions") 35 | private static String setPackageName() { 36 | if (Process.myUid() == 2000) { 37 | return "com.android.shell"; 38 | } else { 39 | return TERMUX_PACKAGES_BUILD_PACKAGE_NAME.startsWith("@") ? 40 | BuildConfig.TERMUX_PACKAGE_NAME : TERMUX_PACKAGES_BUILD_PACKAGE_NAME; 41 | } 42 | } 43 | 44 | @Override 45 | public String getOpPackageName() { 46 | return PACKAGE_NAME; 47 | } 48 | 49 | @TargetApi(Build.VERSION_CODES.S) 50 | @Override 51 | public AttributionSource getAttributionSource() { 52 | AttributionSource.Builder builder = new AttributionSource.Builder(Process.myUid()); 53 | builder.setPackageName(PACKAGE_NAME); 54 | return builder.build(); 55 | } 56 | 57 | // @Override to be added on SDK upgrade for Android 14 58 | @SuppressWarnings("unused") 59 | public int getDeviceId() { 60 | return 0; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/BaseCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | ** 3 | ** Copyright 2013, The Android Open Source Project 4 | ** 5 | ** Licensed under the Apache License, Version 2.0 (the "License"); 6 | ** you may not use this file except in compliance with the License. 7 | ** You may obtain a copy of the License at 8 | ** 9 | ** http://www.apache.org/licenses/LICENSE-2.0 10 | ** 11 | ** Unless required by applicable law or agreed to in writing, software 12 | ** distributed under the License is distributed on an "AS IS" BASIS, 13 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | ** See the License for the specific language governing permissions and 15 | ** limitations under the License. 16 | */ 17 | 18 | package com.termux.termuxam; 19 | 20 | import java.io.PrintStream; 21 | 22 | /** 23 | * Copied from android-7.0.0_r1 frameworks/base/core/java/com/android/internal/os 24 | */ 25 | public abstract class BaseCommand { 26 | 27 | final protected ShellCommand mArgs = new ShellCommand(); 28 | 29 | // These are magic strings understood by the Eclipse plugin. 30 | public static final String FATAL_ERROR_CODE = "Error type 1"; 31 | public static final String NO_SYSTEM_ERROR_CODE = "Error type 2"; 32 | public static final String NO_CLASS_ERROR_CODE = "Error type 3"; 33 | 34 | /** 35 | * Call to run the command. 36 | */ 37 | public Integer run(String[] args) { 38 | if (args.length < 1) { 39 | onShowUsage(System.out); 40 | return 0; 41 | } 42 | 43 | mArgs.init(args, 0); 44 | 45 | try { 46 | return onRun(); 47 | } catch (IllegalArgumentException e) { 48 | onShowUsage(System.err); 49 | System.err.println(); 50 | System.err.println("Error: " + e.getMessage()); 51 | return 1; 52 | } catch (Exception e) { 53 | e.printStackTrace(System.err); 54 | return 1; 55 | } 56 | } 57 | 58 | /** 59 | * Convenience to show usage information to error output. 60 | */ 61 | public void showUsage() { 62 | onShowUsage(System.err); 63 | } 64 | 65 | /** 66 | * Convenience to show usage information to error output along 67 | * with an error message. 68 | */ 69 | public void showError(String message) { 70 | onShowUsage(System.err); 71 | System.err.println(); 72 | System.err.println(message); 73 | } 74 | 75 | /** 76 | * Implement the command. 77 | */ 78 | public abstract Integer onRun() throws Exception; 79 | 80 | /** 81 | * Print help text for the command. 82 | */ 83 | public abstract void onShowUsage(PrintStream out); 84 | 85 | /** 86 | * Return the next option on the command line -- that is an argument that 87 | * starts with '-'. If the next argument is not an option, null is returned. 88 | */ 89 | public String nextOption() { 90 | return mArgs.getNextOption(); 91 | } 92 | 93 | /** 94 | * Return the next argument on the command line, whatever it is; if there are 95 | * no arguments left, return null. 96 | */ 97 | public String nextArg() { 98 | return mArgs.getNextArg(); 99 | } 100 | 101 | /** 102 | * Return the next argument on the command line, whatever it is; if there are 103 | * no arguments left, throws an IllegalArgumentException to report this to the user. 104 | */ 105 | public String nextArgRequired() { 106 | return mArgs.getNextArgRequired(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/ShellCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.termux.termuxam; 18 | 19 | /** 20 | * Copied from android-7.0.0_r1 frameworks/base/core/java/android/os 21 | */ 22 | public class ShellCommand { 23 | static final String TAG = "ShellCommand"; 24 | static final boolean DEBUG = false; 25 | 26 | private String[] mArgs; 27 | 28 | private int mArgPos; 29 | private String mCurArgData; 30 | 31 | public void init(String[] args, int firstArgPos) { 32 | mArgs = args; 33 | mArgPos = firstArgPos; 34 | mCurArgData = null; 35 | } 36 | 37 | /** 38 | * Return the next option on the command line -- that is an argument that 39 | * starts with '-'. If the next argument is not an option, null is returned. 40 | */ 41 | public String getNextOption() { 42 | if (mCurArgData != null) { 43 | String prev = mArgs[mArgPos - 1]; 44 | throw new IllegalArgumentException("No argument expected after \"" + prev + "\""); 45 | } 46 | if (mArgPos >= mArgs.length) { 47 | return null; 48 | } 49 | String arg = mArgs[mArgPos]; 50 | if (!arg.startsWith("-")) { 51 | return null; 52 | } 53 | mArgPos++; 54 | if (arg.equals("--")) { 55 | return null; 56 | } 57 | if (arg.length() > 1 && arg.charAt(1) != '-') { 58 | if (arg.length() > 2) { 59 | mCurArgData = arg.substring(2); 60 | return arg.substring(0, 2); 61 | } else { 62 | mCurArgData = null; 63 | return arg; 64 | } 65 | } 66 | mCurArgData = null; 67 | return arg; 68 | } 69 | 70 | /** 71 | * Return the next argument on the command line, whatever it is; if there are 72 | * no arguments left, return null. 73 | */ 74 | public String getNextArg() { 75 | if (mCurArgData != null) { 76 | String arg = mCurArgData; 77 | mCurArgData = null; 78 | return arg; 79 | } else if (mArgPos < mArgs.length) { 80 | return mArgs[mArgPos++]; 81 | } else { 82 | return null; 83 | } 84 | } 85 | 86 | public String peekNextArg() { 87 | if (mCurArgData != null) { 88 | return mCurArgData; 89 | } else if (mArgPos < mArgs.length) { 90 | return mArgs[mArgPos]; 91 | } else { 92 | return null; 93 | } 94 | } 95 | 96 | /** 97 | * Return the next argument on the command line, whatever it is; if there are 98 | * no arguments left, throws an IllegalArgumentException to report this to the user. 99 | */ 100 | public String getNextArgRequired() { 101 | String arg = getNextArg(); 102 | if (arg == null) { 103 | String prev = mArgs[mArgPos - 1]; 104 | throw new IllegalArgumentException("Argument expected after \"" + prev + "\""); 105 | } 106 | return arg; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/reflection/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam.reflection; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import com.termux.termuxam.logger.Logger; 9 | import com.termux.termuxam.reflection.result.MethodInvokeResult; 10 | 11 | import org.lsposed.hiddenapibypass.HiddenApiBypass; 12 | 13 | import java.lang.reflect.Method; 14 | import java.util.Arrays; 15 | 16 | public class ReflectionUtils { 17 | 18 | private static boolean HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = Build.VERSION.SDK_INT < Build.VERSION_CODES.P; 19 | 20 | /** 21 | * Bypass android hidden API reflection restrictions. 22 | * https://github.com/LSPosed/AndroidHiddenApiBypass 23 | * https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces 24 | */ 25 | public synchronized static void bypassHiddenAPIReflectionRestrictions() { 26 | if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 27 | try { 28 | HiddenApiBypass.addHiddenApiExemptions(""); 29 | } catch (Throwable t) { 30 | Logger.logStackTraceWithMessage("Failed to bypass hidden API reflection restrictions", t); 31 | } 32 | 33 | HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true; 34 | } 35 | } 36 | 37 | /** Check if android hidden API reflection restrictions are bypassed. */ 38 | public synchronized static boolean areHiddenAPIReflectionRestrictionsBypassed() { 39 | return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED; 40 | } 41 | 42 | 43 | 44 | 45 | 46 | /** 47 | * Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters. 48 | */ 49 | @Nullable 50 | public static Method getDeclaredMethod(Class clazz, String methodName) { 51 | return getDeclaredMethod(clazz, methodName, new Class[0]); 52 | } 53 | 54 | /** 55 | * Get a {@link Method} for the specified class with the specified parameters. 56 | * 57 | * @param clazz The {@link Class} for which to return the method. 58 | * @param methodName The name of the {@link Method}. 59 | * @param parameterTypes The parameter types of the method. 60 | * @return Returns the {@link Method} if getting the it was successful, otherwise {@code null}. 61 | */ 62 | @Nullable 63 | public static Method getDeclaredMethod(Class clazz, String methodName, Class... parameterTypes) { 64 | try { 65 | Method method = clazz.getDeclaredMethod(methodName, parameterTypes); 66 | method.setAccessible(true); 67 | return method; 68 | } catch (Exception e) { 69 | Logger.logStackTraceWithMessage("Failed to get" + 70 | " \"" + methodName + "\" method for \"" + (clazz != null ? clazz.getName() : null) + "\"" + 71 | " class with parameter types: " + Arrays.toString(parameterTypes), e); 72 | return null; 73 | } 74 | } 75 | 76 | 77 | /** 78 | * Wrapper for {@link #invokeMethod(Method, Object, Object...)} without arguments. 79 | */ 80 | @NonNull 81 | public static MethodInvokeResult invokeMethod(Method method, Object obj) { 82 | return invokeMethod(method, obj, new Object[0]); 83 | } 84 | 85 | /** 86 | * Invoke a {@link Method} on the specified object with the specified arguments. 87 | * 88 | * @param method The {@link Method} to invoke. 89 | * @param obj The {@link Object} the method should be invoked from. 90 | * @param args The arguments to pass to the method. 91 | * @return Returns the {@link MethodInvokeResult} of invoking the method. The 92 | * {@link MethodInvokeResult#success} will be {@code true} if invoking the method was successful, 93 | * otherwise {@code false}. The {@link MethodInvokeResult#value} will contain the {@link Object} 94 | * returned by the method. 95 | */ 96 | @NonNull 97 | public static MethodInvokeResult invokeMethod(Method method, Object obj, Object... args) { 98 | try { 99 | method.setAccessible(true); 100 | return new MethodInvokeResult(true, method.invoke(obj, args)); 101 | } catch (Exception e) { 102 | Logger.logStackTraceWithMessage("Failed to invoke" + 103 | " \"" + (method != null ? method.getName() : null) + "\" method with object" + 104 | " \"" + obj + "\" and args: " + Arrays.toString(args), e); 105 | return new MethodInvokeResult(false, null); 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/PermissionUtils.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import static com.termux.termuxam.Am.LOG_TAG; 4 | 5 | import android.Manifest; 6 | import android.annotation.SuppressLint; 7 | import android.app.AppOpsManager; 8 | import android.content.Context; 9 | import android.os.Build; 10 | import android.os.IBinder; 11 | import android.os.Process; 12 | import android.os.ServiceManager; 13 | import android.util.Log; 14 | 15 | import com.android.internal.app.IAppOpsService; 16 | 17 | import java.lang.reflect.InvocationTargetException; 18 | 19 | public class PermissionUtils { 20 | 21 | @SuppressWarnings({"JavaReflectionMemberAccess", "SameParameterValue", "deprecation"}) 22 | @SuppressLint("PrivateApi") 23 | static boolean checkPermission(String op) { 24 | try { 25 | // We do not need Context to use checkOpNoThrow/unsafeCheckOpNoThrow so it can be null 26 | IBinder binder = ServiceManager.getService("appops"); 27 | IAppOpsService service = IAppOpsService.Stub.asInterface(binder); 28 | AppOpsManager appops = (AppOpsManager) Class.forName("android.app.AppOpsManager") 29 | .getDeclaredConstructor(Context.class, IAppOpsService.class) 30 | .newInstance(null, service); 31 | 32 | if (appops == null) return false; 33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 34 | int allowed; 35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 36 | allowed = appops.unsafeCheckOpNoThrow(op, Process.myUid(), FakeContext.PACKAGE_NAME); 37 | } else { 38 | allowed = appops.checkOpNoThrow(op, Process.myUid(), FakeContext.PACKAGE_NAME); 39 | } 40 | 41 | return (allowed == AppOpsManager.MODE_ALLOWED); 42 | } else { 43 | return false; 44 | } 45 | } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | 46 | InstantiationException | InvocationTargetException e) { 47 | e.printStackTrace(); 48 | } 49 | return false; 50 | } 51 | 52 | /** 53 | * Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission has been granted. 54 | * 55 | * @return Returns {@code true} if permission is granted, otherwise {@code false}. 56 | */ 57 | public static boolean checkDisplayOverOtherAppsPermission() { 58 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) 59 | return checkPermission(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW); 60 | else 61 | return true; 62 | } 63 | 64 | /** 65 | * Check if running on sdk 29 (android 10) or higher and {@link Manifest.permission#SYSTEM_ALERT_WINDOW} 66 | * permission has been granted or not. 67 | * 68 | * @param logGranted If it should be logged that permission has been granted. 69 | * @param logNotGranted If it should be logged that permission has not been granted. 70 | * @return Returns {@code true} if permission is granted, otherwise {@code false}. 71 | */ 72 | public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(boolean logGranted, boolean logNotGranted) { 73 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true; 74 | 75 | if (!checkDisplayOverOtherAppsPermission()) { 76 | if (logNotGranted) 77 | Log.w(LOG_TAG, "The " + FakeContext.PACKAGE_NAME + " package does not have" + 78 | " the Display over other apps (SYSTEM_ALERT_WINDOW) permission"); 79 | return false; 80 | } else { 81 | if (logGranted) 82 | Log.d(LOG_TAG, "The " + FakeContext.PACKAGE_NAME + " package already has" + 83 | " the Display over other apps (SYSTEM_ALERT_WINDOW) permission"); 84 | return true; 85 | } 86 | } 87 | 88 | public static String getMissingDisplayOverOtherAppsPermissionError() { 89 | return "The " + FakeContext.PACKAGE_NAME + " app requires the" + 90 | " \"Display over other apps\" permission to start activities and" + 91 | " services from background on Android >= 10. Grants it from Android" + 92 | " Settings -> Apps -> %1$s -> Advanced -> Draw over other apps.\n" + 93 | "The permission name may be different on different devices, like" + 94 | " on Xiaomi, its called \"Display pop-up windows while running" + 95 | " in the background\".\n" + 96 | "Check https://dontkillmyapp.com for device specific issues."; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/termux/termuxam/IActivityManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.IIntentReceiver; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.os.IBinder; 11 | 12 | import com.termux.termuxam.test.TestActivity; 13 | import com.termux.termuxam.test.TestComponentsService; 14 | import com.termux.termuxam.test.TestReceiver; 15 | import com.termux.termuxam.test.TestService; 16 | 17 | import org.junit.After; 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | import org.lsposed.hiddenapibypass.HiddenApiBypass; 22 | 23 | import java.lang.reflect.Field; 24 | import java.util.concurrent.CountDownLatch; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertNotNull; 29 | import static org.junit.Assert.assertTrue; 30 | 31 | import androidx.test.InstrumentationRegistry; 32 | import androidx.test.ext.junit.runners.AndroidJUnit4; 33 | 34 | 35 | @RunWith(AndroidJUnit4.class) 36 | public class IActivityManagerTest { 37 | 38 | private IActivityManager mAm; 39 | private String mAction; 40 | 41 | private ITestComponentsService mTestComponentsService; 42 | private ServiceConnection mServiceConnection; 43 | 44 | @Before 45 | public void setUp() throws Exception { 46 | try { 47 | HiddenApiBypass.addHiddenApiExemptions(""); 48 | } catch (Throwable t) { 49 | throw new RuntimeException(t); 50 | } 51 | 52 | mAm = new IActivityManager(InstrumentationRegistry.getTargetContext().getPackageName(), true); 53 | 54 | // Generate Intent action for use in tests 55 | mAction = "com.termux.termuxam.test.TEST_INTENT_" + Math.random(); 56 | 57 | // Connect to test components service 58 | final CountDownLatch serviceConnectedLatch = new CountDownLatch(1); 59 | mServiceConnection = new ServiceConnection() { 60 | @Override 61 | public void onServiceConnected(ComponentName name, IBinder service) { 62 | mTestComponentsService = ITestComponentsService.Stub.asInterface(service); 63 | serviceConnectedLatch.countDown(); 64 | } 65 | 66 | @Override 67 | public void onServiceDisconnected(ComponentName name) {} 68 | }; 69 | InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 70 | @Override 71 | public void run() { 72 | Intent intent = new Intent(InstrumentationRegistry.getContext(), TestComponentsService.class); 73 | InstrumentationRegistry.getTargetContext().bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); 74 | } 75 | }); 76 | serviceConnectedLatch.await(); 77 | } 78 | 79 | @After 80 | public void tearDown() throws Exception { 81 | InstrumentationRegistry.getTargetContext().unbindService(mServiceConnection); 82 | } 83 | 84 | @Test 85 | public void testMethodsAvailable() throws Exception { 86 | 87 | for (Field field : IActivityManager.class.getDeclaredFields()) { 88 | if (field.getType() == CrossVersionReflectedMethod.class) { 89 | 90 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU && 91 | field.getName().equals("mGetProviderMimeTypeMethod")) { 92 | continue; 93 | } 94 | 95 | field.setAccessible(true); 96 | CrossVersionReflectedMethod method = (CrossVersionReflectedMethod) field.get(mAm); 97 | assertTrue(field.getName(), method != null && method.isFound()); 98 | } 99 | } 100 | } 101 | 102 | @Test 103 | public void testStartActivity() throws Exception { 104 | mTestComponentsService.prepareAwait(); 105 | Intent intent = new Intent(mAction, null, InstrumentationRegistry.getContext(), TestActivity.class); 106 | mAm.startActivityAsUser(intent, null, 0, null, 0); 107 | assertEquals("TestActivity " + mAction, mTestComponentsService.await()); 108 | } 109 | 110 | @Test 111 | public void testBroadcastIntent() throws Exception { 112 | // Do not run on Android `>= 14` since it will trigger the 113 | // `Sending broadcast with resultTo requires resultToApp` exception in logcat by 114 | // ActivityManagerService and will hang the test forever 115 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) return; 116 | 117 | final CountDownLatch latch = new CountDownLatch(1); 118 | final Intent[] outIntent = new Intent[1]; 119 | final String[] outData = new String[1]; 120 | IIntentReceiver finishReceiver = new IIntentReceiver.Stub() { 121 | @Override 122 | public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { 123 | outIntent[0] = intent; 124 | outData[0] = data; 125 | latch.countDown(); 126 | } 127 | }; 128 | 129 | // Send the broadcast 130 | mAm.broadcastIntent(new Intent(mAction, null, InstrumentationRegistry.getContext(), TestReceiver.class), finishReceiver, null, true, false, 0); 131 | 132 | // Wait for result and check values 133 | latch.await(); 134 | boolean notTimedOut = latch.await(3, TimeUnit.SECONDS); 135 | assertTrue(notTimedOut); 136 | assertNotNull(outIntent[0]); 137 | assertEquals(mAction, outIntent[0].getAction()); 138 | assertEquals(TestReceiver.REPLY_DATA + mAction, outData[0]); 139 | } 140 | 141 | @Test 142 | public void testStartStopService() throws Exception { 143 | Intent intent = new Intent(mAction, null, InstrumentationRegistry.getContext(), TestService.class); 144 | // Start service 145 | mTestComponentsService.prepareAwait(); 146 | mAm.startService(intent, null, 0); 147 | assertEquals("Start TestService " + mAction, mTestComponentsService.await()); 148 | // Stop service 149 | mTestComponentsService.prepareAwait(); 150 | mAm.stopService(intent, null, 0); 151 | assertEquals("Stop TestService", mTestComponentsService.await()); 152 | } 153 | } -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/ActivityManager.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | /** 4 | * \@hide-hidden constants 5 | */ 6 | public class ActivityManager { 7 | private static final int FIRST_START_FATAL_ERROR_CODE = -100; 8 | private static final int LAST_START_FATAL_ERROR_CODE = -1; 9 | private static final int FIRST_START_SUCCESS_CODE = 0; 10 | private static final int LAST_START_SUCCESS_CODE = 99; 11 | private static final int FIRST_START_NON_FATAL_ERROR_CODE = 100; 12 | private static final int LAST_START_NON_FATAL_ERROR_CODE = 199; 13 | 14 | /** 15 | * Result for IActivityManager.startVoiceActivity: active session is currently hidden. 16 | * @hide 17 | */ 18 | public static final int START_VOICE_HIDDEN_SESSION = FIRST_START_FATAL_ERROR_CODE; 19 | 20 | /** 21 | * Result for IActivityManager.startVoiceActivity: active session does not match 22 | * the requesting token. 23 | * @hide 24 | */ 25 | public static final int START_VOICE_NOT_ACTIVE_SESSION = FIRST_START_FATAL_ERROR_CODE + 1; 26 | 27 | /** 28 | * Result for IActivityManager.startActivity: trying to start a background user 29 | * activity that shouldn't be displayed for all users. 30 | * @hide 31 | */ 32 | public static final int START_NOT_CURRENT_USER_ACTIVITY = FIRST_START_FATAL_ERROR_CODE + 2; 33 | 34 | /** 35 | * Result for IActivityManager.startActivity: trying to start an activity under voice 36 | * control when that activity does not support the VOICE category. 37 | * @hide 38 | */ 39 | public static final int START_NOT_VOICE_COMPATIBLE = FIRST_START_FATAL_ERROR_CODE + 3; 40 | 41 | /** 42 | * Result for IActivityManager.startActivity: an error where the 43 | * start had to be canceled. 44 | * @hide 45 | */ 46 | public static final int START_CANCELED = FIRST_START_FATAL_ERROR_CODE + 4; 47 | 48 | /** 49 | * Result for IActivityManager.startActivity: an error where the 50 | * thing being started is not an activity. 51 | * @hide 52 | */ 53 | public static final int START_NOT_ACTIVITY = FIRST_START_FATAL_ERROR_CODE + 5; 54 | 55 | /** 56 | * Result for IActivityManager.startActivity: an error where the 57 | * caller does not have permission to start the activity. 58 | * @hide 59 | */ 60 | public static final int START_PERMISSION_DENIED = FIRST_START_FATAL_ERROR_CODE + 6; 61 | 62 | /** 63 | * Result for IActivityManager.startActivity: an error where the 64 | * caller has requested both to forward a result and to receive 65 | * a result. 66 | * @hide 67 | */ 68 | public static final int START_FORWARD_AND_REQUEST_CONFLICT = FIRST_START_FATAL_ERROR_CODE + 7; 69 | 70 | /** 71 | * Result for IActivityManager.startActivity: an error where the 72 | * requested class is not found. 73 | * @hide 74 | */ 75 | public static final int START_CLASS_NOT_FOUND = FIRST_START_FATAL_ERROR_CODE + 8; 76 | 77 | /** 78 | * Result for IActivityManager.startActivity: an error where the 79 | * given Intent could not be resolved to an activity. 80 | * @hide 81 | */ 82 | public static final int START_INTENT_NOT_RESOLVED = FIRST_START_FATAL_ERROR_CODE + 9; 83 | 84 | /** 85 | * Result for IActivityManager.startAssistantActivity: active session is currently hidden. 86 | * @hide 87 | */ 88 | public static final int START_ASSISTANT_HIDDEN_SESSION = FIRST_START_FATAL_ERROR_CODE + 10; 89 | 90 | /** 91 | * Result for IActivityManager.startAssistantActivity: active session does not match 92 | * the requesting token. 93 | * @hide 94 | */ 95 | public static final int START_ASSISTANT_NOT_ACTIVE_SESSION = FIRST_START_FATAL_ERROR_CODE + 11; 96 | 97 | /** 98 | * Result for IActivityManaqer.startActivity: the activity was started 99 | * successfully as normal. 100 | * @hide 101 | */ 102 | public static final int START_SUCCESS = FIRST_START_SUCCESS_CODE; 103 | 104 | /** 105 | * Result for IActivityManaqer.startActivity: the caller asked that the Intent not 106 | * be executed if it is the recipient, and that is indeed the case. 107 | * @hide 108 | */ 109 | public static final int START_RETURN_INTENT_TO_CALLER = FIRST_START_SUCCESS_CODE + 1; 110 | 111 | /** 112 | * Result for IActivityManaqer.startActivity: activity wasn't really started, but 113 | * a task was simply brought to the foreground. 114 | * @hide 115 | */ 116 | public static final int START_TASK_TO_FRONT = FIRST_START_SUCCESS_CODE + 2; 117 | 118 | /** 119 | * Result for IActivityManaqer.startActivity: activity wasn't really started, but 120 | * the given Intent was given to the existing top activity. 121 | * @hide 122 | */ 123 | public static final int START_DELIVERED_TO_TOP = FIRST_START_SUCCESS_CODE + 3; 124 | 125 | /** 126 | * Result for IActivityManaqer.startActivity: request was canceled because 127 | * app switches are temporarily canceled to ensure the user's last request 128 | * (such as pressing home) is performed. 129 | * @hide 130 | */ 131 | public static final int START_SWITCHES_CANCELED = FIRST_START_NON_FATAL_ERROR_CODE; 132 | 133 | /** 134 | * Result for IActivityManaqer.startActivity: a new activity was attempted to be started 135 | * while in Lock Task Mode. 136 | * @hide 137 | */ 138 | public static final int START_RETURN_LOCK_TASK_MODE_VIOLATION = 139 | FIRST_START_NON_FATAL_ERROR_CODE + 1; 140 | 141 | /** 142 | * Result for IActivityManaqer.startActivity: a new activity start was aborted. Never returned 143 | * externally. 144 | * @hide 145 | */ 146 | public static final int START_ABORTED = FIRST_START_NON_FATAL_ERROR_CODE + 2; 147 | 148 | /** 149 | * Flag for IActivityManaqer.startActivity: do special start mode where 150 | * a new activity is launched only if it is needed. 151 | * @hide 152 | */ 153 | public static final int START_FLAG_ONLY_IF_NEEDED = 1<<0; 154 | 155 | /** 156 | * Flag for IActivityManaqer.startActivity: launch the app for 157 | * debugging. 158 | * @hide 159 | */ 160 | public static final int START_FLAG_DEBUG = 1<<1; 161 | 162 | /** 163 | * Flag for IActivityManaqer.startActivity: launch the app for 164 | * allocation tracking. 165 | * @hide 166 | */ 167 | public static final int START_FLAG_TRACK_ALLOCATION = 1<<2; 168 | 169 | /** 170 | * Flag for IActivityManaqer.startActivity: launch the app with 171 | * native debugging support. 172 | * @hide 173 | */ 174 | public static final int START_FLAG_NATIVE_DEBUGGING = 1<<3; 175 | 176 | /** 177 | * Result for IActivityManaqer.broadcastIntent: success! 178 | * @hide 179 | */ 180 | public static final int BROADCAST_SUCCESS = 0; 181 | 182 | /** 183 | * Result for IActivityManaqer.broadcastIntent: attempt to broadcast 184 | * a sticky intent without appropriate permission. 185 | * @hide 186 | */ 187 | public static final int BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1; 188 | 189 | /** 190 | * Result for IActivityManager.broadcastIntent: trying to send a broadcast 191 | * to a stopped user. Fail. 192 | * @hide 193 | */ 194 | public static final int BROADCAST_FAILED_USER_STOPPED = -2; 195 | 196 | 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/Workarounds.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import static com.termux.termuxam.Am.LOG_TAG; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.app.Application; 7 | import android.content.Context; 8 | import android.content.ContextWrapper; 9 | import android.content.pm.ApplicationInfo; 10 | import android.os.Build; 11 | import android.os.Looper; 12 | import android.util.Log; 13 | 14 | import java.lang.reflect.Constructor; 15 | import java.lang.reflect.Field; 16 | import java.lang.reflect.Method; 17 | 18 | /** 19 | * - https://github.com/Genymobile/scrcpy/blob/v2.1.1/server/src/main/java/com/genymobile/scrcpy/Workarounds.java 20 | */ 21 | public class Workarounds { 22 | 23 | private static Class activityThreadClass; 24 | private static Object activityThread; 25 | 26 | private Workarounds() { 27 | // not instantiable 28 | } 29 | 30 | public static void applyMaybeAudio(boolean audio) { 31 | boolean mustFillAppInfo = false; 32 | boolean mustFillBaseContext = false; 33 | boolean mustFillAppContext = false; 34 | 35 | 36 | if (Build.BRAND.equalsIgnoreCase("meizu")) { 37 | // Workarounds must be applied for Meizu phones: 38 | // - 39 | // - 40 | // - 41 | // 42 | // But only apply when strictly necessary, since workarounds can cause other issues: 43 | // - 44 | // - 45 | mustFillAppInfo = true; 46 | } else if (Build.BRAND.equalsIgnoreCase("honor")) { 47 | // More workarounds must be applied for Honor devices: 48 | // - 49 | // 50 | // The system context must not be set for all devices, because it would cause other problems: 51 | // - 52 | // - 53 | mustFillAppInfo = true; 54 | mustFillBaseContext = true; 55 | mustFillAppContext = true; 56 | } 57 | 58 | if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { 59 | // Before Android 11, audio is not supported. 60 | // Since Android 12, we can properly set a context on the AudioRecord. 61 | // Only on Android 11 we must fill the application context for the AudioRecord to work. 62 | mustFillAppContext = true; 63 | } 64 | 65 | apply(mustFillAppInfo, mustFillBaseContext, mustFillAppContext); 66 | } 67 | 68 | public static void apply(boolean mustFillAppInfo, boolean mustFillBaseContext, boolean mustFillAppContext) { 69 | if (mustFillAppInfo) { 70 | Workarounds.fillAppInfo(); 71 | } 72 | if (mustFillBaseContext) { 73 | Workarounds.fillBaseContext(); 74 | } 75 | if (mustFillAppContext) { 76 | Workarounds.fillAppContext(); 77 | } 78 | } 79 | 80 | @SuppressWarnings("deprecation") 81 | private static void prepareMainLooper() { 82 | // Some devices internally create a Handler when creating an input Surface, causing an exception: 83 | // "Can't create handler inside thread that has not called Looper.prepare()" 84 | // 85 | // 86 | // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: 87 | // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' 88 | // on a null object reference" 89 | // 90 | Looper.prepareMainLooper(); 91 | } 92 | 93 | public static Object getActivityThread() throws Exception { 94 | setActivityThread(); 95 | return activityThread; 96 | } 97 | 98 | @SuppressLint("PrivateApi,DiscouragedPrivateApi") 99 | private static void setActivityThread() throws Exception { 100 | if (activityThread == null) { 101 | Workarounds.prepareMainLooper(); 102 | 103 | // ActivityThread activityThread = new ActivityThread(); 104 | activityThreadClass = Class.forName("android.app.ActivityThread"); 105 | Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); 106 | activityThreadConstructor.setAccessible(true); 107 | activityThread = activityThreadConstructor.newInstance(); 108 | } 109 | } 110 | 111 | private static void fillActivityThread() throws Exception { 112 | setActivityThread(); 113 | 114 | // ActivityThread.sCurrentActivityThread = activityThread; 115 | Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); 116 | sCurrentActivityThreadField.setAccessible(true); 117 | sCurrentActivityThreadField.set(null, activityThread); 118 | } 119 | 120 | @SuppressLint("PrivateApi,DiscouragedPrivateApi") 121 | private static void fillAppInfo() { 122 | try { 123 | fillActivityThread(); 124 | 125 | // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); 126 | Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); 127 | Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); 128 | appBindDataConstructor.setAccessible(true); 129 | Object appBindData = appBindDataConstructor.newInstance(); 130 | 131 | ApplicationInfo applicationInfo = new ApplicationInfo(); 132 | applicationInfo.packageName = FakeContext.PACKAGE_NAME; 133 | 134 | // appBindData.appInfo = applicationInfo; 135 | Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); 136 | appInfoField.setAccessible(true); 137 | appInfoField.set(appBindData, applicationInfo); 138 | 139 | // activityThread.mBoundApplication = appBindData; 140 | Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); 141 | mBoundApplicationField.setAccessible(true); 142 | mBoundApplicationField.set(activityThread, appBindData); 143 | } catch (Throwable t) { 144 | // this is a workaround, so failing is not an error 145 | Log.d(LOG_TAG, "Could not fill app info. message: " + t.getMessage() + ", cause" + t.getCause()); 146 | } 147 | } 148 | 149 | @SuppressLint("PrivateApi,DiscouragedPrivateApi") 150 | private static void fillAppContext() { 151 | try { 152 | fillActivityThread(); 153 | 154 | Application app = Application.class.newInstance(); 155 | Field baseField = ContextWrapper.class.getDeclaredField("mBase"); 156 | baseField.setAccessible(true); 157 | baseField.set(app, FakeContext.get()); 158 | 159 | // activityThread.mInitialApplication = app; 160 | Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); 161 | mInitialApplicationField.setAccessible(true); 162 | mInitialApplicationField.set(activityThread, app); 163 | } catch (Throwable t) { 164 | // this is a workaround, so failing is not an error 165 | Log.d(LOG_TAG, "Could not fill app context. message: " + t.getMessage() + ", cause" + t.getCause()); 166 | } 167 | } 168 | 169 | public static void fillBaseContext() { 170 | try { 171 | fillActivityThread(); 172 | 173 | Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); 174 | Context context = (Context) getSystemContextMethod.invoke(activityThread); 175 | FakeContext.get().setBaseContext(context); 176 | } catch (Throwable t) { 177 | // this is a workaround, so failing is not an error 178 | Log.d(LOG_TAG, "Could not fill base context. message: " + t.getMessage() + ", cause" + t.getCause()); 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/CrossVersionReflectedMethod.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.HashMap; 6 | 7 | /** 8 | * Class wrapping reflection method and using named arguments for invocation 9 | * 10 | * Can have multiple variants of method and find one that actually exists 11 | */ 12 | public class CrossVersionReflectedMethod { 13 | 14 | private final Class mClass; 15 | private Method mMethod = null; 16 | private Object[] mDefaultArgs; 17 | private HashMap mArgNamesToIndexes; 18 | 19 | 20 | public CrossVersionReflectedMethod(Class aClass) { 21 | mClass = aClass; 22 | } 23 | 24 | 25 | /** 26 | * Try finding method method variant in reflected class 27 | * 28 | * @param methodName Name of method to be found 29 | * @param typesNamesAndDefaults 30 | * any amount of (in order, all required for each set) 31 | * - Types (as class, used in reflection) 32 | * - Names (used for {@link #invoke(Object, Object...)} call) 33 | * - Default values 34 | */ 35 | public CrossVersionReflectedMethod tryMethodVariant(String methodName, Object... typesNamesAndDefaults) { 36 | // If we have already found an implementation, skip next checks 37 | if (mMethod != null) { 38 | return this; 39 | } 40 | 41 | try { 42 | // Get list of arguments for reflection call 43 | int argCount = typesNamesAndDefaults.length / 3; 44 | Class[] refArguments = new Class[argCount]; 45 | for (int i = 0; i < argCount; i++) { 46 | Object refArgument = typesNamesAndDefaults[i * 3]; 47 | if (refArgument instanceof Class) { 48 | refArguments[i] = (Class) refArgument; 49 | } else { 50 | refArguments[i] = Class.forName((String) refArgument); 51 | } 52 | 53 | } 54 | 55 | // Get method 56 | mMethod = mClass.getMethod(methodName, (Class[]) refArguments); 57 | 58 | // If we're here - method exists 59 | mArgNamesToIndexes = new HashMap<>(); 60 | mDefaultArgs = new Object[argCount]; 61 | for (int i = 0; i < argCount; i++) { 62 | mArgNamesToIndexes.put((String) typesNamesAndDefaults[i * 3 + 1], i); 63 | mDefaultArgs[i] = typesNamesAndDefaults[i * 3 + 2]; 64 | } 65 | } catch (NoSuchMethodException | ClassNotFoundException ignored) {} 66 | return this; 67 | } 68 | 69 | /** 70 | * Try finding method method variant in reflected class, 71 | * allowing method in class to have additional arguments between provided ones 72 | * 73 | * @param methodName Name of method to be found 74 | * @param typesNamesAndDefaults 75 | * any amount of (in order, all required for each set) 76 | * - Types (as class, used in reflection) 77 | * - Names (used for {@link #invoke(Object, Object...)} call) 78 | * - Default values 79 | */ 80 | public CrossVersionReflectedMethod tryMethodVariantInexact(String methodName, Object... typesNamesAndDefaults) { 81 | // If we have already found an implementation, skip next checks 82 | if (mMethod != null) { 83 | return this; 84 | } 85 | 86 | int expectedArgCount = typesNamesAndDefaults.length / 3; 87 | 88 | for (Method method : mClass.getMethods()) { 89 | if (!methodName.equals(method.getName())) { 90 | continue; 91 | } 92 | 93 | // Matched name, try matching arguments 94 | // Get list of arguments for reflection call 95 | 96 | try { 97 | // These are for arguments provided to this method 98 | Class expectedArgumentClass = null; 99 | int expectedArgumentI = 0; 100 | 101 | // This is for method arguments found through reflection 102 | int actualArgumentI = 0; 103 | 104 | // Parameters for method - we'll copy them to fields 105 | // when we're sure that this is right method 106 | HashMap argNamesToIndexes = new HashMap<>(); 107 | Object[] defaultArgs = new Object[method.getParameterTypes().length]; 108 | 109 | // Iterate over actual method arguments 110 | for (Class methodParam : method.getParameterTypes()) { 111 | // Get expected argument if we haven't it cached 112 | if (expectedArgumentClass == null && expectedArgumentI < expectedArgCount) { 113 | Object refArgument = typesNamesAndDefaults[expectedArgumentI * 3]; 114 | if (refArgument instanceof Class) { 115 | expectedArgumentClass = (Class) refArgument; 116 | } else { 117 | expectedArgumentClass = Class.forName((String) refArgument); 118 | } 119 | } 120 | 121 | // Check if this argument is expected one 122 | if (methodParam == expectedArgumentClass) { 123 | argNamesToIndexes.put((String) typesNamesAndDefaults[expectedArgumentI * 3 + 1], actualArgumentI); 124 | defaultArgs[actualArgumentI] = typesNamesAndDefaults[expectedArgumentI * 3 + 2]; 125 | 126 | // Note this argument is passed 127 | expectedArgumentI++; 128 | expectedArgumentClass = null; 129 | } else { 130 | defaultArgs[actualArgumentI] = getDefaultValueForPrimitiveClass(methodParam); 131 | } 132 | 133 | actualArgumentI++; 134 | } 135 | 136 | // Check if method has all requested arguments 137 | if (expectedArgumentI != expectedArgCount) { 138 | continue; 139 | } 140 | 141 | // Export result if matched 142 | mMethod = method; 143 | mDefaultArgs = defaultArgs; 144 | mArgNamesToIndexes = argNamesToIndexes; 145 | } catch (ClassNotFoundException e) { 146 | // No such class on this system, probably okay 147 | /*if (BuildConfig.DEBUG) { 148 | e.printStackTrace(); 149 | }*/ 150 | } 151 | } 152 | return this; 153 | } 154 | 155 | /** 156 | * Invoke method 157 | * 158 | * @param receiver Object on which we call {@link Method#invoke(Object, Object...)} 159 | * @param namesAndValues 160 | * Any amount of argument name (as used in {@link #tryMethodVariant(String, Object...)} and value pairs 161 | */ 162 | public Object invoke(Object receiver, Object ...namesAndValues) throws InvocationTargetException { 163 | if (mMethod == null) { 164 | throw new RuntimeException("Couldn't find method with matching signature"); 165 | } 166 | Object[] args = mDefaultArgs.clone(); 167 | for (int i = 0; i < namesAndValues.length; i += 2) { 168 | @SuppressWarnings("SuspiciousMethodCalls") 169 | Integer namedArgIndex = mArgNamesToIndexes.get(namesAndValues[i]); 170 | if (namedArgIndex != null) { 171 | args[namedArgIndex] = namesAndValues[i + 1]; 172 | } 173 | } 174 | try { 175 | return mMethod.invoke(receiver, args); 176 | } catch (IllegalAccessException e) { 177 | throw new RuntimeException(e); 178 | } 179 | } 180 | 181 | public static Object getDefaultValueForPrimitiveClass(Class aClass) { 182 | if (aClass == Boolean.TYPE) { 183 | return false; 184 | } else if (aClass == Byte.TYPE) { 185 | return (byte) 0; 186 | } else if (aClass == Character.TYPE) { 187 | return 0; 188 | } else if (aClass == Short.TYPE) { 189 | return (short) 0; 190 | } else if (aClass == Integer.TYPE) { 191 | return 0; 192 | } else if (aClass == Long.TYPE) { 193 | return (long) 0; 194 | } else if (aClass == Float.TYPE) { 195 | return 0; 196 | } else if (aClass == Double.TYPE) { 197 | return 0; 198 | } else { 199 | return null; 200 | } 201 | } 202 | 203 | public boolean isFound() { 204 | return mMethod != null; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/IActivityManager.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.PendingIntent; 5 | import android.content.ComponentName; 6 | import android.content.IIntentReceiver; 7 | import android.content.Intent; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Bundle; 11 | import android.os.IBinder; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | 15 | /** 16 | * Wrapper around android.app.IActivityManager internal interface 17 | */ 18 | @SuppressLint("PrivateApi") 19 | class IActivityManager { 20 | 21 | private Object mAm; 22 | private final String mCallingAppPackage; 23 | private CrossVersionReflectedMethod mGetProviderMimeTypeMethod; 24 | private CrossVersionReflectedMethod mStartActivityAsUserMethod; 25 | //private CrossVersionReflectedMethod mBroadcastIntentMethod; 26 | private CrossVersionReflectedMethod mStartServiceMethod; 27 | private CrossVersionReflectedMethod mStopServiceMethod; 28 | private CrossVersionReflectedMethod mGetIntentSenderMethod; 29 | private CrossVersionReflectedMethod mIntentSenderSendMethod; 30 | 31 | IActivityManager() throws Exception { 32 | this(FakeContext.PACKAGE_NAME, false); 33 | } 34 | 35 | IActivityManager(String callingAppPackage, boolean setupMethods) throws Exception { 36 | mCallingAppPackage = callingAppPackage; 37 | 38 | try { 39 | try { 40 | mAm = android.app.ActivityManager.class 41 | .getMethod("getService") 42 | .invoke(null); 43 | } catch (Exception e) { 44 | mAm = Class.forName("android.app.ActivityManagerNative") 45 | .getMethod("getDefault") 46 | .invoke(null); 47 | } 48 | } catch (Exception e) { 49 | throw new RuntimeException(e); 50 | } 51 | 52 | if (setupMethods) { 53 | getGetProviderMimeTypeMethod(); 54 | getStartActivityAsUserMethod(); 55 | //getBroadcastIntentMethod(); 56 | getStartServiceMethod(); 57 | getStopServiceMethod(); 58 | getGetIntentSenderMethod(); 59 | getIntentSenderSendMethod(); 60 | } 61 | } 62 | 63 | private CrossVersionReflectedMethod getGetProviderMimeTypeMethod() { 64 | if (mGetProviderMimeTypeMethod != null) return mGetProviderMimeTypeMethod; 65 | 66 | try { 67 | mGetProviderMimeTypeMethod = new CrossVersionReflectedMethod(mAm.getClass()) 68 | .tryMethodVariantInexact( 69 | "getProviderMimeType", 70 | Uri.class, "uri", 71 | int.class, "userId" 72 | ); 73 | } catch (Exception e) { 74 | throw new RuntimeException(e); 75 | } 76 | 77 | return mGetProviderMimeTypeMethod; 78 | } 79 | 80 | private CrossVersionReflectedMethod getStartActivityAsUserMethod() { 81 | if (mStartActivityAsUserMethod != null) return mStartActivityAsUserMethod; 82 | 83 | try { 84 | mStartActivityAsUserMethod = new CrossVersionReflectedMethod(mAm.getClass()) 85 | .tryMethodVariantInexact( 86 | "startActivityAsUser", 87 | "android.app.IApplicationThread", "caller", null, 88 | String.class, "callingPackage", mCallingAppPackage, 89 | Intent.class, "intent", null, 90 | String.class, "resolvedType", null, 91 | IBinder.class, "resultTo", null, 92 | String.class, "resultWho", null, 93 | int.class, "requestCode", -1, 94 | int.class, "flags", 0, 95 | //ProfilerInfo profilerInfo, - let's autodetect 96 | Bundle.class, "options", null, 97 | int.class, "userId", 0 98 | ); 99 | } catch (Exception e) { 100 | throw new RuntimeException(e); 101 | } 102 | 103 | return mStartActivityAsUserMethod; 104 | } 105 | 106 | /* 107 | private CrossVersionReflectedMethod getBroadcastIntentMethod() { 108 | if (mBroadcastIntentMethod != null) return mBroadcastIntentMethod; 109 | 110 | try { 111 | mBroadcastIntentMethod = new CrossVersionReflectedMethod(mAm.getClass()) 112 | .tryMethodVariantInexact( 113 | "broadcastIntent", 114 | "android.app.IApplicationThread", "caller", null, 115 | Intent.class, "intent", null, 116 | String.class, "resolvedType", null, 117 | IIntentReceiver.class, "resultTo", null, 118 | int.class, "resultCode", -1, 119 | String.class, "resultData", null, 120 | Bundle.class, "map", null, 121 | String[].class, "requiredPermissions", null, 122 | int.class, "appOp", 0, 123 | Bundle.class, "options", null, 124 | boolean.class, "serialized", false, 125 | boolean.class, "sticky", false, 126 | int.class, "userId", 0 127 | ); 128 | } catch (Exception e) { 129 | throw new RuntimeException(e); 130 | } 131 | 132 | return mBroadcastIntentMethod; 133 | } 134 | */ 135 | 136 | private CrossVersionReflectedMethod getStartServiceMethod() { 137 | if (mStartServiceMethod != null) return mStartServiceMethod; 138 | 139 | try { 140 | mStartServiceMethod = new CrossVersionReflectedMethod(mAm.getClass()) 141 | .tryMethodVariantInexact( 142 | "startService", 143 | "android.app.IApplicationThread", "caller", null, 144 | Intent.class, "service", null, 145 | String.class, "resolvedType", null, 146 | boolean.class, "requireForeground", false, 147 | String.class, "callingPackage", mCallingAppPackage, 148 | int.class, "userId", 0 149 | ).tryMethodVariantInexact( 150 | "startService", 151 | "android.app.IApplicationThread", "caller", null, 152 | Intent.class, "service", null, 153 | String.class, "resolvedType", null, 154 | String.class, "callingPackage", mCallingAppPackage, 155 | int.class, "userId", 0 156 | ).tryMethodVariantInexact( // Pre frameworks/base 99b6043 157 | "startService", 158 | "android.app.IApplicationThread", "caller", null, 159 | Intent.class, "service", null, 160 | String.class, "resolvedType", null, 161 | int.class, "userId", 0 162 | ); 163 | } catch (Exception e) { 164 | throw new RuntimeException(e); 165 | } 166 | 167 | return mStartServiceMethod; 168 | } 169 | 170 | private CrossVersionReflectedMethod getStopServiceMethod() { 171 | if (mStopServiceMethod != null) return mStopServiceMethod; 172 | 173 | try { 174 | mStopServiceMethod = new CrossVersionReflectedMethod(mAm.getClass()) 175 | .tryMethodVariantInexact( 176 | "stopService", 177 | "android.app.IApplicationThread", "caller", null, 178 | Intent.class, "service", null, 179 | String.class, "resolvedType", null, 180 | int.class, "userId", 0 181 | ); 182 | } catch (Exception e) { 183 | throw new RuntimeException(e); 184 | } 185 | 186 | return mStopServiceMethod; 187 | } 188 | 189 | private CrossVersionReflectedMethod getGetIntentSenderMethod() { 190 | if (mGetIntentSenderMethod != null) return mGetIntentSenderMethod; 191 | 192 | try { 193 | mGetIntentSenderMethod = new CrossVersionReflectedMethod(mAm.getClass()) 194 | .tryMethodVariantInexact( 195 | "getIntentSender", 196 | int.class, "type", 0, 197 | String.class, "packageName", mCallingAppPackage, 198 | IBinder.class, "token", null, 199 | String.class, "resultWho", null, 200 | int.class, "requestCode", 0, 201 | Intent[].class, "intents", null, 202 | String[].class, "resolvedTypes", null, 203 | int.class, "flags", 0, 204 | Bundle.class, "options", null, 205 | int.class, "userId", 0 206 | ); 207 | } catch (Exception e) { 208 | throw new RuntimeException(e); 209 | } 210 | 211 | return mGetIntentSenderMethod; 212 | } 213 | 214 | private CrossVersionReflectedMethod getIntentSenderSendMethod() { 215 | if (mIntentSenderSendMethod != null) return mIntentSenderSendMethod; 216 | 217 | try { 218 | mIntentSenderSendMethod = new CrossVersionReflectedMethod( 219 | Class.forName("android.content.IIntentSender")) 220 | .tryMethodVariantInexact( 221 | "send", 222 | int.class, "code", 0, 223 | Intent.class, "intent", null, 224 | String.class, "resolvedType", null, 225 | //IBinder.class, "android.os.IBinder whitelistToken", null, 226 | "android.content.IIntentReceiver", "finishedReceiver", null, 227 | String.class, "requiredPermission", null, 228 | Bundle.class, "options", null 229 | ).tryMethodVariantInexact( // Pre frameworks/base a750a63 230 | "send", 231 | int.class, "code", 0, 232 | Intent.class, "intent", null, 233 | String.class, "resolvedType", null, 234 | "android.content.IIntentReceiver", "finishedReceiver", null, 235 | String.class, "requiredPermission", null 236 | ); 237 | } catch (Exception e) { 238 | throw new RuntimeException(e); 239 | } 240 | 241 | return mIntentSenderSendMethod; 242 | } 243 | 244 | 245 | 246 | int startActivityAsUser(Intent intent, String resolvedType, int flags, Bundle options, int userId) throws InvocationTargetException { 247 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S || Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) 248 | Workarounds.apply(true, false, false); 249 | 250 | return (Integer) getStartActivityAsUserMethod().invoke( 251 | mAm, 252 | "intent", intent, 253 | "resolvedType", resolvedType, 254 | "flags", flags, 255 | "options", options, 256 | "userId", userId 257 | ); 258 | } 259 | 260 | void broadcastIntent(Intent intent, IIntentReceiver resultTo, String[] requiredPermissions, 261 | boolean serialized, boolean sticky, int userId) throws InvocationTargetException { 262 | /* 263 | getBroadcastIntentMethod().invoke( 264 | mAm, 265 | "intent", intent, 266 | "resultTo", resultTo, 267 | "requiredPermissions", requiredPermissions, 268 | "serialized", serialized, 269 | "sticky", sticky, 270 | "userId", userId 271 | ); 272 | */ 273 | Object pendingIntent = getGetIntentSenderMethod().invoke( 274 | mAm, 275 | "type", 1 /*ActivityManager.INTENT_SENDER_BROADCAST*/, 276 | "intents", new Intent[] { intent }, 277 | "flags", PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT, 278 | "userId", userId 279 | ); 280 | getIntentSenderSendMethod().invoke( 281 | pendingIntent, 282 | "requiredPermission", (requiredPermissions == null || requiredPermissions.length == 0) ? null : requiredPermissions[0], 283 | "finishedReceiver", resultTo 284 | ); 285 | } 286 | 287 | String getProviderMimeType(Uri uri, int userId) throws InvocationTargetException { 288 | if (!"content".equals(uri.getScheme())) return null; 289 | 290 | // The `IActivityManager.getProviderMimeType()` method has been removed in Android `>= 14` and 291 | // replaced by `getMimeTypeFilterAsync()` (previously `getProviderMimeTypeAsync()`, which 292 | // is an async method that requires a `RemoteCallback` instance to be passed. 293 | // `ActivityManagerShellCommand` also now instead uses `Intent.resolveType()` to get 294 | // mime type, which uses `ContentResolver.getType()` internally and that requires a `Context`. 295 | // The `ContentResolver.getType()` internally calls `IActivityManager.getProviderMimeType()`, 296 | // if failed to get an existing `IContentProvider` with `ActivityThread`, which then calls 297 | // `ContentProviderHelper.getProviderMimeTypeAsync()`, however calling it or 298 | // `getProviderMimeType()` just seems to always return `null` and not call target provider 299 | // for some reason. The `IContentProvider.getTypeAsync()` should call the target provider, 300 | // but to actually get the `IContentProvider` with `IActivityManager.getContentProvider()` 301 | // requires passing an `IApplicationThread` that belongs to an app process since 302 | // `ContentProviderHelper.getContentProviderImpl()` uses that to get the relevant 303 | // `ProcessRecord`, and ir will otherwise throw `Unable to find app for caller` error. 304 | // But since we are not running in an app we don't have a valid `IApplicationThread`, 305 | // even if we get it from `ActivityThread.mAppThread` we create. 306 | // `ActivityThread.acquireExistingProvider()` method used by `ContextImpl`, it is not used. 307 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/core/java/android/app/IActivityManager.aidl;l=339 308 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java;l=497 309 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/core/java/android/content/ContentResolver.java;l=906 310 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/core/java/android/content/Intent.java;l=8563 311 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/services/core/java/com/android/server/am/ContentProviderHelper.java;l=1030 312 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/core/java/android/app/ActivityThread.java;l=7187 313 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/core/java/android/app/ContextImpl.java;l=3222 314 | // - https://cs.android.com/android/platform/superproject/+/android-13.0.0_r74:frameworks/base/services/core/java/com/android/server/am/ContentProviderHelper.java;l=178 315 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { 316 | CrossVersionReflectedMethod method = getGetProviderMimeTypeMethod(); 317 | if (method.isFound()) { 318 | return (String) getGetProviderMimeTypeMethod().invoke( 319 | mAm, 320 | "uri", uri, 321 | "userId", userId 322 | ); 323 | } 324 | } 325 | 326 | return null; 327 | } 328 | 329 | ComponentName startService(Intent service, String resolvedType, int userId) throws InvocationTargetException { 330 | return (ComponentName) getStartServiceMethod().invoke( 331 | mAm, 332 | "service", service, 333 | "resolvedType", resolvedType, 334 | "userId", userId 335 | ); 336 | } 337 | 338 | int stopService(Intent service, String resolvedType, int userId) throws InvocationTargetException { 339 | return (Integer) getStopServiceMethod().invoke( 340 | mAm, 341 | "service", service, 342 | "resolvedType", resolvedType, 343 | "userId", userId 344 | ); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /app/src/main/java/com/termux/termuxam/IntentCmd.java: -------------------------------------------------------------------------------- 1 | package com.termux.termuxam; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | 8 | import java.io.PrintWriter; 9 | import java.net.URISyntaxException; 10 | import java.util.ArrayList; 11 | import java.util.HashSet; 12 | 13 | /** 14 | * Copied from android-7.0.0_r1 frameworks/base/core/java/android/content/Intent.java 15 | */ 16 | public class IntentCmd { 17 | /** @hide */ 18 | public interface CommandOptionHandler { 19 | boolean handleOption(String opt, ShellCommand cmd); 20 | } 21 | 22 | /** @hide */ 23 | public static Intent parseCommandArgs(ShellCommand cmd, CommandOptionHandler optionHandler) 24 | throws URISyntaxException { 25 | Intent intent = new Intent(); 26 | Intent baseIntent = intent; 27 | boolean hasIntentInfo = false; 28 | 29 | Uri data = null; 30 | String type = null; 31 | 32 | String opt; 33 | while ((opt=cmd.getNextOption()) != null) { 34 | switch (opt) { 35 | case "-a": 36 | intent.setAction(cmd.getNextArgRequired()); 37 | if (intent == baseIntent) { 38 | hasIntentInfo = true; 39 | } 40 | break; 41 | case "-d": 42 | data = Uri.parse(cmd.getNextArgRequired()); 43 | if (intent == baseIntent) { 44 | hasIntentInfo = true; 45 | } 46 | break; 47 | case "-t": 48 | type = cmd.getNextArgRequired(); 49 | if (intent == baseIntent) { 50 | hasIntentInfo = true; 51 | } 52 | break; 53 | case "-c": 54 | intent.addCategory(cmd.getNextArgRequired()); 55 | if (intent == baseIntent) { 56 | hasIntentInfo = true; 57 | } 58 | break; 59 | case "-e": 60 | case "--es": { 61 | String key = cmd.getNextArgRequired(); 62 | String value = cmd.getNextArgRequired(); 63 | intent.putExtra(key, value); 64 | } 65 | break; 66 | case "--esn": { 67 | String key = cmd.getNextArgRequired(); 68 | intent.putExtra(key, (String) null); 69 | } 70 | break; 71 | case "--ei": { 72 | String key = cmd.getNextArgRequired(); 73 | String value = cmd.getNextArgRequired(); 74 | intent.putExtra(key, Integer.decode(value)); 75 | } 76 | break; 77 | case "--eu": { 78 | String key = cmd.getNextArgRequired(); 79 | String value = cmd.getNextArgRequired(); 80 | intent.putExtra(key, Uri.parse(value)); 81 | } 82 | break; 83 | case "--ecn": { 84 | String key = cmd.getNextArgRequired(); 85 | String value = cmd.getNextArgRequired(); 86 | ComponentName cn = ComponentName.unflattenFromString(value); 87 | if (cn == null) 88 | throw new IllegalArgumentException("Bad component name: " + value); 89 | intent.putExtra(key, cn); 90 | } 91 | break; 92 | case "--eia": { 93 | String key = cmd.getNextArgRequired(); 94 | String value = cmd.getNextArgRequired(); 95 | String[] strings = value.split(","); 96 | int[] list = new int[strings.length]; 97 | for (int i = 0; i < strings.length; i++) { 98 | list[i] = Integer.decode(strings[i]); 99 | } 100 | intent.putExtra(key, list); 101 | } 102 | break; 103 | case "--eial": { 104 | String key = cmd.getNextArgRequired(); 105 | String value = cmd.getNextArgRequired(); 106 | String[] strings = value.split(","); 107 | ArrayList list = new ArrayList<>(strings.length); 108 | for (int i = 0; i < strings.length; i++) { 109 | list.add(Integer.decode(strings[i])); 110 | } 111 | intent.putExtra(key, list); 112 | } 113 | break; 114 | case "--el": { 115 | String key = cmd.getNextArgRequired(); 116 | String value = cmd.getNextArgRequired(); 117 | intent.putExtra(key, Long.valueOf(value)); 118 | } 119 | break; 120 | case "--ela": { 121 | String key = cmd.getNextArgRequired(); 122 | String value = cmd.getNextArgRequired(); 123 | String[] strings = value.split(","); 124 | long[] list = new long[strings.length]; 125 | for (int i = 0; i < strings.length; i++) { 126 | list[i] = Long.valueOf(strings[i]); 127 | } 128 | intent.putExtra(key, list); 129 | hasIntentInfo = true; 130 | } 131 | break; 132 | case "--elal": { 133 | String key = cmd.getNextArgRequired(); 134 | String value = cmd.getNextArgRequired(); 135 | String[] strings = value.split(","); 136 | ArrayList list = new ArrayList<>(strings.length); 137 | for (int i = 0; i < strings.length; i++) { 138 | list.add(Long.valueOf(strings[i])); 139 | } 140 | intent.putExtra(key, list); 141 | hasIntentInfo = true; 142 | } 143 | break; 144 | case "--ef": { 145 | String key = cmd.getNextArgRequired(); 146 | String value = cmd.getNextArgRequired(); 147 | intent.putExtra(key, Float.valueOf(value)); 148 | hasIntentInfo = true; 149 | } 150 | break; 151 | case "--efa": { 152 | String key = cmd.getNextArgRequired(); 153 | String value = cmd.getNextArgRequired(); 154 | String[] strings = value.split(","); 155 | float[] list = new float[strings.length]; 156 | for (int i = 0; i < strings.length; i++) { 157 | list[i] = Float.valueOf(strings[i]); 158 | } 159 | intent.putExtra(key, list); 160 | hasIntentInfo = true; 161 | } 162 | break; 163 | case "--efal": { 164 | String key = cmd.getNextArgRequired(); 165 | String value = cmd.getNextArgRequired(); 166 | String[] strings = value.split(","); 167 | ArrayList list = new ArrayList<>(strings.length); 168 | for (int i = 0; i < strings.length; i++) { 169 | list.add(Float.valueOf(strings[i])); 170 | } 171 | intent.putExtra(key, list); 172 | hasIntentInfo = true; 173 | } 174 | break; 175 | case "--esa": { 176 | String key = cmd.getNextArgRequired(); 177 | String value = cmd.getNextArgRequired(); 178 | // Split on commas unless they are preceeded by an escape. 179 | // The escape character must be escaped for the string and 180 | // again for the regex, thus four escape characters become one. 181 | String[] strings = value.split("(? list = new ArrayList<>(strings.length); 197 | for (int i = 0; i < strings.length; i++) { 198 | list.add(strings[i].replaceAll("\\\\,",",")); 199 | } 200 | intent.putExtra(key, list); 201 | hasIntentInfo = true; 202 | } 203 | break; 204 | case "--ez": { 205 | String key = cmd.getNextArgRequired(); 206 | String value = cmd.getNextArgRequired().toLowerCase(); 207 | // Boolean.valueOf() results in false for anything that is not "true", which is 208 | // error-prone in shell commands 209 | boolean arg; 210 | if ("true".equals(value) || "t".equals(value)) { 211 | arg = true; 212 | } else if ("false".equals(value) || "f".equals(value)) { 213 | arg = false; 214 | } else { 215 | try { 216 | arg = Integer.decode(value) != 0; 217 | } catch (NumberFormatException ex) { 218 | throw new IllegalArgumentException("Invalid boolean value: " + value); 219 | } 220 | } 221 | 222 | intent.putExtra(key, arg); 223 | } 224 | break; 225 | case "-n": { 226 | String str = cmd.getNextArgRequired(); 227 | ComponentName cn = ComponentName.unflattenFromString(str); 228 | if (cn == null) 229 | throw new IllegalArgumentException("Bad component name: " + str); 230 | intent.setComponent(cn); 231 | if (intent == baseIntent) { 232 | hasIntentInfo = true; 233 | } 234 | } 235 | break; 236 | case "-p": { 237 | String str = cmd.getNextArgRequired(); 238 | intent.setPackage(str); 239 | if (intent == baseIntent) { 240 | hasIntentInfo = true; 241 | } 242 | } 243 | break; 244 | case "-f": 245 | String str = cmd.getNextArgRequired(); 246 | intent.setFlags(Integer.decode(str).intValue()); 247 | break; 248 | case "--grant-read-uri-permission": 249 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 250 | break; 251 | case "--grant-write-uri-permission": 252 | intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 253 | break; 254 | case "--grant-persistable-uri-permission": 255 | intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 256 | break; 257 | case "--grant-prefix-uri-permission": 258 | intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 259 | break; 260 | case "--exclude-stopped-packages": 261 | intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES); 262 | break; 263 | case "--include-stopped-packages": 264 | intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); 265 | break; 266 | case "--debug-log-resolution": 267 | intent.addFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION); 268 | break; 269 | case "--activity-brought-to-front": 270 | intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); 271 | break; 272 | case "--activity-clear-top": 273 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 274 | break; 275 | case "--activity-clear-when-task-reset": 276 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 277 | break; 278 | case "--activity-exclude-from-recents": 279 | intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); 280 | break; 281 | case "--activity-launched-from-history": 282 | intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); 283 | break; 284 | case "--activity-multiple-task": 285 | intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 286 | break; 287 | case "--activity-no-animation": 288 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 289 | break; 290 | case "--activity-no-history": 291 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); 292 | break; 293 | case "--activity-no-user-action": 294 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); 295 | break; 296 | case "--activity-previous-is-top": 297 | intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 298 | break; 299 | case "--activity-reorder-to-front": 300 | intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); 301 | break; 302 | case "--activity-reset-task-if-needed": 303 | intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 304 | break; 305 | case "--activity-single-top": 306 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 307 | break; 308 | case "--activity-clear-task": 309 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 310 | break; 311 | case "--activity-task-on-home": 312 | intent.addFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME); 313 | break; 314 | case "--receiver-registered-only": 315 | intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 316 | break; 317 | case "--receiver-replace-pending": 318 | intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); 319 | break; 320 | case "--receiver-foreground": 321 | intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 322 | break; 323 | case "--receiver-no-abort": 324 | intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT); 325 | break; 326 | /* 327 | case "--receiver-include-background": 328 | intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 329 | break; 330 | */ 331 | case "--selector": 332 | intent.setDataAndType(data, type); 333 | intent = new Intent(); 334 | break; 335 | default: 336 | if (optionHandler != null && optionHandler.handleOption(opt, cmd)) { 337 | // Okay, caller handled this option. 338 | } else { 339 | throw new IllegalArgumentException("Unknown option: " + opt); 340 | } 341 | break; 342 | } 343 | } 344 | intent.setDataAndType(data, type); 345 | 346 | final boolean hasSelector = intent != baseIntent; 347 | if (hasSelector) { 348 | // A selector was specified; fix up. 349 | baseIntent.setSelector(intent); 350 | intent = baseIntent; 351 | } 352 | 353 | String arg = cmd.getNextArg(); 354 | baseIntent = null; 355 | if (arg == null) { 356 | if (hasSelector) { 357 | // If a selector has been specified, and no arguments 358 | // have been supplied for the main Intent, then we can 359 | // assume it is ACTION_MAIN CATEGORY_LAUNCHER; we don't 360 | // need to have a component name specified yet, the 361 | // selector will take care of that. 362 | baseIntent = new Intent(Intent.ACTION_MAIN); 363 | baseIntent.addCategory(Intent.CATEGORY_LAUNCHER); 364 | } 365 | } else if (arg.indexOf(':') >= 0) { 366 | // The argument is a URI. Fully parse it, and use that result 367 | // to fill in any data not specified so far. 368 | baseIntent = Intent.parseUri(arg, Intent.URI_INTENT_SCHEME 369 | | Intent.URI_ANDROID_APP_SCHEME | Intent.URI_ALLOW_UNSAFE); 370 | } else if (arg.indexOf('/') >= 0) { 371 | // The argument is a component name. Build an Intent to launch 372 | // it. 373 | baseIntent = new Intent(Intent.ACTION_MAIN); 374 | baseIntent.addCategory(Intent.CATEGORY_LAUNCHER); 375 | baseIntent.setComponent(ComponentName.unflattenFromString(arg)); 376 | } else { 377 | // Assume the argument is a package name. 378 | baseIntent = new Intent(Intent.ACTION_MAIN); 379 | baseIntent.addCategory(Intent.CATEGORY_LAUNCHER); 380 | baseIntent.setPackage(arg); 381 | } 382 | if (baseIntent != null) { 383 | Bundle extras = intent.getExtras(); 384 | intent.replaceExtras((Bundle)null); 385 | Bundle uriExtras = baseIntent.getExtras(); 386 | baseIntent.replaceExtras((Bundle)null); 387 | if (intent.getAction() != null && baseIntent.getCategories() != null) { 388 | HashSet cats = new HashSet(baseIntent.getCategories()); 389 | for (String c : cats) { 390 | baseIntent.removeCategory(c); 391 | } 392 | } 393 | intent.fillIn(baseIntent, Intent.FILL_IN_COMPONENT | Intent.FILL_IN_SELECTOR); 394 | if (extras == null) { 395 | extras = uriExtras; 396 | } else if (uriExtras != null) { 397 | uriExtras.putAll(extras); 398 | extras = uriExtras; 399 | } 400 | intent.replaceExtras(extras); 401 | hasIntentInfo = true; 402 | } 403 | 404 | if (!hasIntentInfo) throw new IllegalArgumentException("No intent supplied"); 405 | return intent; 406 | } 407 | 408 | /** @hide */ 409 | public static void printIntentArgsHelp(PrintWriter pw, String prefix) { 410 | final String[] lines = new String[] { 411 | " specifications include these flags and arguments:", 412 | " [-a ] [-d ] [-t ]", 413 | " [-c [-c ] ...]", 414 | " [-e|--es ...]", 415 | " [--esn ...]", 416 | " [--ez ...]", 417 | " [--ei ...]", 418 | " [--el ...]", 419 | " [--ef ...]", 420 | " [--eu ...]", 421 | " [--ecn ]", 422 | " [--eia [, [,)", 426 | " [--ela [, [,)", 430 | " [--efa [, [,)", 434 | " [--esa [, [,; to embed a comma into a string,", 439 | " escape it using \"\\,\")", 440 | " [-f ]", 441 | " [--grant-read-uri-permission] [--grant-write-uri-permission]", 442 | " [--grant-persistable-uri-permission] [--grant-prefix-uri-permission]", 443 | " [--debug-log-resolution] [--exclude-stopped-packages]", 444 | " [--include-stopped-packages]", 445 | " [--activity-brought-to-front] [--activity-clear-top]", 446 | " [--activity-clear-when-task-reset] [--activity-exclude-from-recents]", 447 | " [--activity-launched-from-history] [--activity-multiple-task]", 448 | " [--activity-no-animation] [--activity-no-history]", 449 | " [--activity-no-user-action] [--activity-previous-is-top]", 450 | " [--activity-reorder-to-front] [--activity-reset-task-if-needed]", 451 | " [--activity-single-top] [--activity-clear-task]", 452 | " [--activity-task-on-home]", 453 | " [--receiver-registered-only] [--receiver-replace-pending]", 454 | " [--receiver-foreground] [--receiver-no-abort]", 455 | " [--receiver-include-background]", 456 | " [--selector]", 457 | " [ | | ]" 458 | }; 459 | for (String line : lines) { 460 | pw.print(prefix); 461 | pw.println(line); 462 | } 463 | } 464 | 465 | } 466 | --------------------------------------------------------------------------------