
Enables compile-time access to hidden system APIs via ASM bytecode transformation, remapping types and resolving hidden overload method conflicts through annotation-based configuration.
English | 简体中文
A Gradle plugin that uses ASM bytecode transformation to enable compile-time access to Android hidden APIs.
# gradle/libs.versions.toml
[versions]
remap = "<version>" # https://github.com/lisonge/remap/releases
[libraries]
remap-processor = { module = "li.songe.remap:remap-processor", version.ref = "remap" }
remap-annotation = { module = "li.songe.remap:remap-annotation", version.ref = "remap" }
[plugins]
remap = { id = "li.songe.remap", version.ref = "remap" }// build.gradle.kts
plugins {
alias(libs.plugins.remap) apply false
}// hidden-api/build.gradle.kts
dependencies {
compileOnly(libs.remap.annotation)
annotationProcessor(libs.remap.processor)
}// app/build.gradle.kts
plugins {
alias(libs.plugins.remap)
}
dependencies {
remapApi(project(":hidden-api"))
}Remap supports the Android target provided by
com.android.kotlin.multiplatform.library. Apply it to the KMP/CMP module and
keep remapApi in the module's top-level dependencies block:
plugins {
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
id("li.songe.remap")
}
kotlin {
android {
namespace = "example.shared"
compileSdk = 36
}
jvm()
}
dependencies {
remapApi(project(":hidden-api"))
}The plugin supplies stubs to src/androidMain through androidMainCompileOnly
and transforms Android main classes. Remap supports both plain KMP and Compose
Multiplatform projects.
Place the stubs and annotation processor in a separate :hidden-api module.
For modules with build types or product flavors, select the matching variant
with kotlin.android.localDependencySelection as needed.
Apply Remap to each Android module that uses hidden API stubs. Keep all stubs in
one module and connect it with remapApi(project(...)); this dependency is used
at compile time.
For public APIs consumed by other projects, prefer SDK types or your own types. Consumers of signatures or inline bodies that reference stubs also need Remap and the same stub dependency.
// hidden-api/src/main/java/android/app/AppOpsManagerHidden.java
package android.app;
import android.os.Build;
import li.songe.remap.RemapType;
@RemapType(AppOpsManager.class)
public class AppOpsManagerHidden {
public static int OP_POST_NOTIFICATION;
public static String opToName(int op) {
throw new RuntimeException();
}
public void clearHistory() {
throw new RuntimeException();
}
}AppOpsManagerHidden is a hidden type, it will be remapped to AppOpsManager.
// app/src/main/kotlin/example/app/Example.kt
package example.app
import android.app.AppOpsManager
import android.app.AppOpsManagerHidden
fun test(manger: AppOpsManager){
manger is AppOpsManagerHidden // true
AppOpsManagerHidden.opToName(AppOpsManagerHidden.OP_POST_NOTIFICATION) // "POST_NOTIFICATION"
(manger as AppOpsManagerHidden).clearHistory() // will Clears all app ops history
}Interface fields are implicitly static final. Initialize stub fields with
RemapStub.value() so calling code reads the actual system field at runtime.
// hidden-api/src/main/java/android/os/IBinderHidden.java
package android.os;
import li.songe.remap.RemapStub;
import li.songe.remap.RemapType;
@RemapType(IBinder.class)
public interface IBinderHidden {
int SHELL_COMMAND_TRANSACTION = RemapStub.value();
}Place these declarations in the compile-time :hidden-api module. Remap maps
field references in calling code to the system type.
Using RemapStub.value() for interface fields has an R8 compatibility issue: an internal compiler crash has been reproduced in some consumers with R8 9.4.24. Two workarounds are available:
Declare the field through an implementing class or subclass. Find a system class that inherits the field, map the stub to that class, and declare an uninitialized public static field to retain runtime access. For example, access IBinder.SHELL_COMMAND_TRANSACTION through Binder:
@RemapType(Binder.class)
public class BinderHidden {
public static int SHELL_COMMAND_TRANSACTION;
}Use the constant value from source. For protocol constants verified to be stable across your supported Android versions, copy the value from AOSP into application code to replace the hidden field access. For example, SHELL_COMMAND_TRANSACTION:
public static final int SHELL_COMMAND_TRANSACTION = 0x5f434d44;Use the second approach for constants whose version stability you have checked. The value is available in AOSP's IBinder source.
// hidden-api/src/main/java/android/content/IPackageManager.java
package android.content;
import android.os.IInterface;
import li.songe.remap.RemapMethod;
public interface IPackageManager extends IInterface {
// android8 - android12L
ParceledListSlice<PackageInfo> getInstalledPackages(int flags, int userId);
// android13 - android16
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
// android17+
// The return type changed; use an alias for the new signature.
@RemapMethod("getInstalledPackages")
PackageInfoList getInstalledPackagesV17(long flags, int userId);
}getInstalledPackagesV17 is a hidden overload conflict method, it will be remapped to getInstalledPackages.
you can check its signature changes at https://diff.songe.li/i/IPackageManager.getInstalledPackages
// app/src/main/kotlin/example/app/Example.kt
package example.app
import android.content.IPackageManager
import android.content.PackageInfo
import android.content.PackageInfoList
import android.os.Build
fun test(manger: IPackageManager, flags: Long, userId: Int): List<PackageInfo> {
return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) { // android17+
manger.getInstalledPackagesV17(flags, userId)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // android13 - android16
manger.getInstalledPackages(flags, userId)
} else { // android8 - android12L
manger.getInstalledPackages(flags.toInt(), userId)
}).list
}Remap automatically generates -dontwarn rules for hidden types and
-keepclassmembers rules for system callbacks. Android and KMP libraries package
these rules in their AAR; application modules use them directly.
Unused classes can still be removed, and class names and unrelated members can
still be obfuscated. Reflection and callbacks absent from the stubs need separate rules.
See R8 rule generation (Chinese) for output locations and examples of generated keep rules. See the development notes for implementation details, compatibility cases, and development checks.
English | 简体中文
A Gradle plugin that uses ASM bytecode transformation to enable compile-time access to Android hidden APIs.
# gradle/libs.versions.toml
[versions]
remap = "<version>" # https://github.com/lisonge/remap/releases
[libraries]
remap-processor = { module = "li.songe.remap:remap-processor", version.ref = "remap" }
remap-annotation = { module = "li.songe.remap:remap-annotation", version.ref = "remap" }
[plugins]
remap = { id = "li.songe.remap", version.ref = "remap" }// build.gradle.kts
plugins {
alias(libs.plugins.remap) apply false
}// hidden-api/build.gradle.kts
dependencies {
compileOnly(libs.remap.annotation)
annotationProcessor(libs.remap.processor)
}// app/build.gradle.kts
plugins {
alias(libs.plugins.remap)
}
dependencies {
remapApi(project(":hidden-api"))
}Remap supports the Android target provided by
com.android.kotlin.multiplatform.library. Apply it to the KMP/CMP module and
keep remapApi in the module's top-level dependencies block:
plugins {
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
id("li.songe.remap")
}
kotlin {
android {
namespace = "example.shared"
compileSdk = 36
}
jvm()
}
dependencies {
remapApi(project(":hidden-api"))
}The plugin supplies stubs to src/androidMain through androidMainCompileOnly
and transforms Android main classes. Remap supports both plain KMP and Compose
Multiplatform projects.
Place the stubs and annotation processor in a separate :hidden-api module.
For modules with build types or product flavors, select the matching variant
with kotlin.android.localDependencySelection as needed.
Apply Remap to each Android module that uses hidden API stubs. Keep all stubs in
one module and connect it with remapApi(project(...)); this dependency is used
at compile time.
For public APIs consumed by other projects, prefer SDK types or your own types. Consumers of signatures or inline bodies that reference stubs also need Remap and the same stub dependency.
// hidden-api/src/main/java/android/app/AppOpsManagerHidden.java
package android.app;
import android.os.Build;
import li.songe.remap.RemapType;
@RemapType(AppOpsManager.class)
public class AppOpsManagerHidden {
public static int OP_POST_NOTIFICATION;
public static String opToName(int op) {
throw new RuntimeException();
}
public void clearHistory() {
throw new RuntimeException();
}
}AppOpsManagerHidden is a hidden type, it will be remapped to AppOpsManager.
// app/src/main/kotlin/example/app/Example.kt
package example.app
import android.app.AppOpsManager
import android.app.AppOpsManagerHidden
fun test(manger: AppOpsManager){
manger is AppOpsManagerHidden // true
AppOpsManagerHidden.opToName(AppOpsManagerHidden.OP_POST_NOTIFICATION) // "POST_NOTIFICATION"
(manger as AppOpsManagerHidden).clearHistory() // will Clears all app ops history
}Interface fields are implicitly static final. Initialize stub fields with
RemapStub.value() so calling code reads the actual system field at runtime.
// hidden-api/src/main/java/android/os/IBinderHidden.java
package android.os;
import li.songe.remap.RemapStub;
import li.songe.remap.RemapType;
@RemapType(IBinder.class)
public interface IBinderHidden {
int SHELL_COMMAND_TRANSACTION = RemapStub.value();
}Place these declarations in the compile-time :hidden-api module. Remap maps
field references in calling code to the system type.
Using RemapStub.value() for interface fields has an R8 compatibility issue: an internal compiler crash has been reproduced in some consumers with R8 9.4.24. Two workarounds are available:
Declare the field through an implementing class or subclass. Find a system class that inherits the field, map the stub to that class, and declare an uninitialized public static field to retain runtime access. For example, access IBinder.SHELL_COMMAND_TRANSACTION through Binder:
@RemapType(Binder.class)
public class BinderHidden {
public static int SHELL_COMMAND_TRANSACTION;
}Use the constant value from source. For protocol constants verified to be stable across your supported Android versions, copy the value from AOSP into application code to replace the hidden field access. For example, SHELL_COMMAND_TRANSACTION:
public static final int SHELL_COMMAND_TRANSACTION = 0x5f434d44;Use the second approach for constants whose version stability you have checked. The value is available in AOSP's IBinder source.
// hidden-api/src/main/java/android/content/IPackageManager.java
package android.content;
import android.os.IInterface;
import li.songe.remap.RemapMethod;
public interface IPackageManager extends IInterface {
// android8 - android12L
ParceledListSlice<PackageInfo> getInstalledPackages(int flags, int userId);
// android13 - android16
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
// android17+
// The return type changed; use an alias for the new signature.
@RemapMethod("getInstalledPackages")
PackageInfoList getInstalledPackagesV17(long flags, int userId);
}getInstalledPackagesV17 is a hidden overload conflict method, it will be remapped to getInstalledPackages.
you can check its signature changes at https://diff.songe.li/i/IPackageManager.getInstalledPackages
// app/src/main/kotlin/example/app/Example.kt
package example.app
import android.content.IPackageManager
import android.content.PackageInfo
import android.content.PackageInfoList
import android.os.Build
fun test(manger: IPackageManager, flags: Long, userId: Int): List<PackageInfo> {
return (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) { // android17+
manger.getInstalledPackagesV17(flags, userId)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // android13 - android16
manger.getInstalledPackages(flags, userId)
} else { // android8 - android12L
manger.getInstalledPackages(flags.toInt(), userId)
}).list
}Remap automatically generates -dontwarn rules for hidden types and
-keepclassmembers rules for system callbacks. Android and KMP libraries package
these rules in their AAR; application modules use them directly.
Unused classes can still be removed, and class names and unrelated members can
still be obfuscated. Reflection and callbacks absent from the stubs need separate rules.
See R8 rule generation (Chinese) for output locations and examples of generated keep rules. See the development notes for implementation details, compatibility cases, and development checks.