├── 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 |
--------------------------------------------------------------------------------