For a while I used the Xbootclasspath
trick but Android Studio doesn't really like it (highlighting was broken) and it doesn't work at all if you are trying to access hidden APIs that have changed across SDK levels (these APIs are hidden so they are unstable).
Eventually I found a solution that involves creating pure java libraries that are used in my Android project. I call this trick "platform-unhidden" aka P-U. The main annoyance is there is more boilerplate code and the library structure is complex.
If you need to support two levels (lets say API level 25 and lower, and API level 26 and higher) then you will need 5 java libraries. The "stubs" libraries contain Android platform code copied from AOSP but with everything stubbed out like the android.jar in the SDK usually is but with the hidden methods you want exposed.
The "wrap" libraries use the stub libraries and expose methods/classes with unique names. The P-U library selects the appropriate wrapping library based on the detected SDK level with Build.SDK_INT
.
Visually this looks like:
+---------+ +----------+
+--api-->+ wrap-25 +--compile-only-->+ stubs-25 +--+
+-------+ | +---------+ +----------+ | +---------+
| +---+ +-->+ |
| P-U | | android |
| +---+ +-->+ |
+-------+ | +---------+ +----------+ | +---------+
+--api-->+ wrap-26 +--compile-only-->+ stubs-26 +--+
+---------+ +----------+
Setting up the java libraries required creating a shared gradle file like so:
platform-shared.gradle:
// All the platform-* projects apply this gradle file
apply plugin: 'java'
apply plugin: 'java-library'
sourceCompatibility = 1.8
apply from: file("${androidBuild}/versions.gradle")
// ANDROID_HOME can override ANDROID_SDK_ROOT
// see https://developer.android.com/studio/command-line/variables
def androidHome = System.getenv('ANDROID_HOME')
def androidSdkRoot = System.getenv('ANDROID_SDK_ROOT')
if (androidHome?.trim() && new File(androidHome).canRead()) {
androidSdkRoot = androidHome
}
if (!androidSdkRoot?.trim() || !new File(androidSdkRoot).canRead()) {
def props = new Properties()
def file = new File("${rootDir}/local.properties")
if (file.exists()) {
file.withInputStream { props.load(it) }
if (props.getProperty("sdk.dir")) {
androidSdkRoot = props.getProperty("sdk.dir")
}
}
else {
throw new GradleException('Android SDK root not usable')
}
}
repositories {
// Need an Android SDK jar to compile against even though this is a pure java project
repositories {
flatDir {
dirs "${androidSdkRoot}/platforms/android-${COMPILE_SDK_VERSION}"
}
}
mavenCentral()
}
// Make CI happy by implementing these tasks
task assembleRelease {
dependsOn 'jar'
}
task assembleDebug {
dependsOn 'jar'
}
task assembleAndroidTest {
// Ignored
}
task lintRelease {
// Ignored
}
task lintDebug {
// Ignored
}
task testReleaseUnitTest {
// Ignored
}
task testDebugUnitTest {
// Ignored
}
Then you'll need gradle files for each of the libraries like so (not showing 26, it looks the same as 25). Add more levels as needed.
stubs-25.gradle:
apply from: file("../platform-shared.gradle")
dependencies {
api name: "android"
}
wrap-25.gradle:
apply from: file("../platform-shared.gradle")
dependencies {
compileOnly project(':platform-stubs-25')
}
platform-unhidden.gradle:
apply from: file("../platform-shared.gradle")
dependencies {
compileOnly project(':platform-stubs-25')
compileOnly name: "android"
api project(':platform-wrap-25')
api project(':platform-wrap-26')
}