HOME > android > tips

안드로이드 스튜디오, custom framework.jar로 빌드하기

By JS | 30 Nov 2019

Android Studio에서 custom framework.jar로 Hidden api를 사용하는 방법을 소개합니다.

일반적으로 앱은 안드로이드는 SDK 라이브러리로 컴파일됩니다. 구글은 꾸준히 지원할 API를 SDK에 포함시키기 때문에, SDK로 개발하는 것이 안정적입니다.

만약 SDK에 포함되지 않은 hidden API를 사용하여 앱을 개발하고 싶다면 다음과 같은 방법이 있습니다.

  • Reflection을 사용하여 hidden api를 호출
  • Custom framework.jar library를 Android studio에서 import하여 개발

리플렉션을 호출하기에는 가독성이 떨어지고 귀찮기 때문에 custom library를 import하여 안드로이드 스튜디오에서 개발하는 것이 편합니다. 물론 Hidden api는 안드로이드 OS 버전마다 다를 수 있고, 갑자기 사라질 수 있기 때문에 디바이스마다 다르게 동작할 수 있습니다.

Custom framework.jar

안드로이드의 모든 API가 포함된 Custom 라이브러리를 만드는 방법은 안드로이드 소스를 빌드하는 것 뿐입니다.

AOSP 소스를 다운받아 전체빌드를 하면 framework_intermediates 이름의 폴더에 모든 api가 포함된 라이브러리가 생성됩니다.

저는 아래처럼 out 폴더에서 이름으로 검색하여 찾았고, classes.jar가 그 라이브러리 파일입니다.

aosp10/out$ find -name "*framework_intermediates*"
./target/common/obj/JAVA_LIBRARIES/framework_intermediates
./target/product/generic_x86_64/obj_x86/SHARED_LIBRARIES/libdrmframework_intermediates
./target/product/generic_x86_64/obj/JAVA_LIBRARIES/framework_intermediates
./target/product/generic_x86_64/obj/SHARED_LIBRARIES/libdrmframework_intermediates

aosp10/out$ ls ./target/common/obj/JAVA_LIBRARIES/framework_intermediates
classes-header.jar  classes.jar  javalib.jar  link_type

라이브러리가 맞는지 확인하려면 jar tf [jar 파일] 명령어로 파일 안에 hidden class가 포함되어 있는지로 확인할 수 있습니다.

framework_intermediates$ jar tf classes.jar
android/os/storage/StorageManager$StorageEventListenerDelegate.class
android/os/storage/StorageManager$ObbListenerDelegate.class
android/os/storage/StorageManager$ObbListenerDelegate$1.class
....

classes.jar 파일의 이름을 보기 좋게 framework.jar로 변경하고 안드로이드 스튜디오 프로젝트의 app/libs/ 경로에 옴깁니다.

만약 AOSP를 빌드할 환경이 안되신다면 GitHub - CustomFramework에서 "../app/libs/framework.jar"를 다운받아서 사용하세요. Android 10(API 29)에서 빌드한 framework.jar입니다.

프로젝트에서 라이브러리 의존성 추가

라이브러리를 의존성에 추가하는 것은 앱의 build.gradle에 다음과 같이 추가하면 됩니다.

dependencies {
    compileOnly files('libs/framework.jar')

    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
}

compileOnly 옵션은 빌드할 때만 라이브러리를 사용하고 Apk에 라이브러리 코드를 포함시키지 않겠다는 의미입니다. apk가 디바이스에 설치되면 디바이스에 존재하는 framework.jar를 사용하기 때문에 굳이 이 코드를 포함시킬 필요가 없습니다.

우리가 만든 custom library 파일의 크기는 26MB 정도 되거든요. 포함시키면 빌드시간도 오래 걸리고 apk 사이즈도 커집니다.

SDK보다 Custom 라이브러리에서 API를 먼저 찾게 만들기

안드로이드 스튜디오의 프로젝트는 컴파일할 때 사용할 SDK 버전을 미리 설정해 놓습니다. 우리가 이 프로젝트에 Custom framework.jar를 추가해도 SDK에서 클래스를 먼저 찾기 때문에, hidden api를 찾지 못하는 일이 발생합니다.

그래서 앱을 컴파일할 때 SDK보다 Custom 라이브러리의 우선순위를 더 높게 만들어야 합니다.

안드로이드 프로젝트의 ../app/app.iml은 라이브러리의 우선순위를 관리해주는 파일입니다. 이 파일을 잠시 보면 안드로이드 SDK가 커스텀 라이브러리보다 더 우선순위가 높습니다. SDK의 위치를 커스텀 라이브러리 아래로 내려주면 커스텀 라이브러리에서 hidden api를 찾을 수 있습니다.

<orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK"/>
...
<orderEntry type="library" name="Gradle: __local_aars__:/home/mjs/AndroidStudioProjects/CustomFramework/app/libs/framework.jar:unspecified@jar" level="project"/>

app.iml 파일은 gradle이 변경될 때마다 새로 만들어지기 때문에 매번 변경해야 합니다. 그래서 다음과 같이 gradle에 SDK의 위치를 맨 밑으로 옴겨주는 스크립트를 만들면 편합니다.

앱의 gradle에서 dependencies 아래에 아래 스크립트를 추가합니다. 스튜디오에서 Node 등의 클래스를 못찾는다고 에러메시지가 나와도 무시하시면 됩니다. 빌드될 때 잘 동작합니다.

dependencies {
  ...
}

preBuild {
    doLast {
        def imlFile = file( project.name + ".iml")
        println 'Change ' + project.name + '.iml order'
        try {
            def parsedXml = (new XmlParser()).parse(imlFile)
            def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
            parsedXml.component[1].remove(jdkNode)
            def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform"
            println 'what' + sdkString
            new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK'])
            groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
        } catch (FileNotFoundException e) {
            // nop, iml not found
            println "no iml found"
        }
    }
}

그리고 앱이 컴파일 될 때 classpath에 우리가 추가한 라이브러리를 등록해줘야 합니다.

프로젝트의 gradle에 allprojects 아래에 다음과 같이 라이브러리를 클래스 패스에 추가해 줍니다.

allprojects {
    ....

    gradle.projectsEvaluated {
        tasks.withType(JavaCompile) {
            options.compilerArgs.add('-Xbootclasspath/p:app/libs/framework.jar')
        }
    }
}

Hidden api 사용

지금까지 프로젝트 세팅을 모두 마쳤습니다. 이제 hidden api를 사용해보세요.

다음은 AOSP의 UserHandle 코드입니다. static 멤버 변수 CURRENT는 hidden api입니다.

public final class UserHandle
  implements Parcelable
{
  @SystemApi
  public static final UserHandle CURRENT = new UserHandle(-2);
  ...
}

다음과 같은 코드를 앱에서 빌드하여 실행해보면

Log.d("TEST", "UserHandle.CURRENT : " + UserHandle.CURRENT)

이렇게 출력이 됩니다.

11-30 11:02:50.227  7061  7061 D TEST    : UserHandle.CURRENT : UserHandle{-2}

안드로이드 스튜디오에서 hidden api를 못찾는다고.. unresolved reference라도 뜨는데, 컴파일은 잘 됩니다. 예전에는 unresolved가 아니었는데,, 이 글을 쓰려고 샘플을 만들 때는 unresolved라고 뜨네요. 나중에 해결되면 업데이트하겠습니다.

이 글에서 사용한 샘플은 GitHub - CustomFramework에서 확인할 수 있습니다.

참고