Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
SlideShare a Scribd company logo
カスタムROM開発者の視点から
見たAndroid @androidsola
自己紹介
• twitter
@androidsola
• blog
http://blog.sola-dolphin-1.net
• AndroidベースのカスタムROM(JCROM)を開発してます
https://sites.google.com/site/jcromproject
本日の内容
• Androidの概要
• カスタムROM開発を通じて見たAndroid
• SELinuxの視点から見たAndroid
Androidの概要
Androidの概要
AOSP(Android Open Source Project)のサイトで見かける図
Androidの概要
AOSP(Android Open Source Project)のサイトで見かける図
Androidの概要
アプリケーション開発者が見るところ
●
Frameworkに関しては、API
Referenceを見て実装がどう
なってるのかを見たいひとや、
開発したアプリケーションのバ
グ解析時に見るくらい?
Androidの概要
プラットフォーム開発者が見るところ
●
基本的に全て知るのが望ましい
(が、個人や少数では厳しい)
Androidの概要
プラットフォーム開発者が見るところ(主に下回り)
●
新しいターゲット向けに
Androidを移植をするひと
●
既存のターゲットだが、カメラ
やセンサー等のデバイスを変
更、追加するひと
Androidの概要
プラットフォーム開発者が見るところ(Androidの開発)
●
Androidに新しい機能を追加、
改造するひと
●
Applicationも入れてるけど、
メインはFramework
Androidの概要
一部の特殊なひと向け
●
KitkatまではDalvik VM
●
Lollipop以降はART(Android
Run Time)
●
Androidの仮想マシン Dalvik編
という電子書籍がオススメ
http://tatsu-zine.com/books/
androidvm-dalvik
●
KitkatまではDalvik VM
●
Lollipop以降はART(Android
Run Time)
カスタムROM開発を通じて
見たAndroid
JCROMの改造範囲
カスタムROM(JCROM)で改造した範囲
●
KitkatまではDalvik VM
●
Lollipop以降はART(Android
Run Time)
●
改造量の多い順
Framework
Application
HAL
Linux Kernel
JCROMの改造例①
• ナビゲーションバー(変更前)
• ナビゲーションバー(変更後)
JCROMの改造例②
• 通知領域
JCROMの改造例③
• ロックスクリーンへの壁紙の設定(縦用、横用を設定可)
最近の機種によっては変更可能だが、AOSPでは出来ない
• ブートアニメーションの変更
JCROMの改造例①~③の変更範囲
例に挙げた機能を実現するのに変更したところ
JCROMの改造例①~③の変更範囲
• JCROMの場合、ナビゲーションバーや通知領域等の見た目に関わる
ものは、ほぼ全てがSystemUIというものを改造することで実現して
います。
ロックスクリーンはKeyguardとなります。(以前はSystemUIだっ
た)
SystemUI のソースコード
frameworks/base/packages/SystemUI
Keyguardのソースコード
frameworks/base/packages/Keyguard
• ブートアニメーションは少し特殊で、C++で書かれています。
ブートアニメーションのソースコード
frameworks/base/cmds/bootanimation
SystemUIにたどり着くまで
• JCROMのようなカスタムROMを作ろうと考えた当時、何をどう改造
すれば良いのかは知らなかった。
SystemUIにたどり着くまで
• JCROMのようなカスタムROMを作ろうと考えた当時、何をどう改造
すれば良いのかは知らなかった。
• 漠然とナビゲーションバーであれば、キーの画像を差し替えたり
すれば出来るのでは?というところから始まった。
SystemUIにたどり着くまで
• JCROMのようなカスタムROMを作ろうと考えた当時、何をどう改造
すれば良いのかは知らなかった。
• 漠然とナビゲーションバーであれば、キーの画像を差し替えたり
すれば出来るのでは?というところから始まった。
• 最初にやったのはキーの画像を探すこと。
画像ファイル一覧を出力して、それらしい名前を探した。
SystemUIにたどり着くまで
• それらしい名前の画像ファイルが出てくる
frameworks/base/packages/SystemUI/res/drawable-xxxx 配下
- ic_sysbar_home.png
- ic_sysbar_menu.png
- ic_sysbar_back.png
SystemUIにたどり着くまで
• それらしい名前の画像ファイルが出てくる
frameworks/base/packages/SystemUI/res/drawable-xxxx 配下
- ic_sysbar_home.png
- ic_sysbar_menu.png
- ic_sysbar_back.png
• これらの画像ファイルを読み込んでいる箇所を見つけて、自分で
用意した画像ファイルに差し替えた。これが最初の変更。
SystemUIにたどり着くまで
• それらしい名前の画像ファイルが出てくる
frameworks/base/packages/SystemUI/res/drawable-xxxx 配下
- ic_sysbar_home.png
- ic_sysbar_menu.png
- ic_sysbar_back.png
• これらの画像ファイルを読み込んでいる箇所を見つけて、自分で
用意した画像ファイルに差し替えた。これが最初の変更。
• その後、通知領域やステータスバー等に画像を貼ろうとして調べ
ていると、SystemUIにたどり着く。
ブートアニメーションの制御
• ブートアニメーションはビルドすると、/system/bin/bootanimat
ionが作成される
ブートアニメーションの制御
• ブートアニメーションはビルドすると、/system/bin/bootanimat
ionが作成される
• コマンドラインから/system/bin/bootanimationを実行すること
で、ブートアニメーションが表示される
ブートアニメーションの制御
• ブートアニメーションはビルドすると、/system/bin/bootanimat
ionが作成される
• コマンドラインから/system/bin/bootanimationを実行すること
で、ブートアニメーションが表示される
• Android起動後にコマンドラインから/system/bin/bootanimation
を実行すると、すぐに表示が消える
ブートアニメーションの制御
• ブートアニメーションはビルドすると、/system/bin/bootanimat
ionが作成される
• コマンドラインから/system/bin/bootanimationを実行すること
で、ブートアニメーションが表示される
• Android起動後にコマンドラインから/system/bin/bootanimation
を実行すると、すぐに表示が消える
• bootanimationを改造して試そうとした時、この現象に遭遇した
ので、ブートアニメーションの制御(起動・終了)について調査
した
ブートアニメーションの制御
• Androidの起動時、最初に動くのはinitというプロセス
initはinit.rcというファイルを読み込こんで、初期化とサービ
スの起動を行う。
その中にbootanimationは存在する。以下はその抜粋。
service bootanim /system/bin/bootanimation
class core
user graphics
group graphics audio
disabled
oneshot
ブートアニメーションの制御
• init.rcの読み方(サービスの起動)
service bootanim /system/bin/bootanimation
class core : ①class の定義
user graphics : ②user の定義
group graphics audio : ③group の定義
disabled : ④自動起動無し
oneshot : ⑤リスタート無し
 詳細を知るにはドキュメントとソースコードを参照
 ソースコード system/core/init
 ドキュメント system/core/init/readme.txt
ブートアニメーションの制御
• 自動起動無しのため、誰かが起動していることになる
誰が起動してるのかを探してみると、具体的に使用例が分かる
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
開始
 void SurfaceFlinger::startBootAnim() {
  // start boot animation
  property_set("service.bootanim.exit", "0");
  property_set("ctl.start", "bootanim");
 }
終了
 void SurfaceFlinger::bootFinished()
 { //省略
  property_set("service.bootanim.exit", "1");
 }
ブートアニメーションの制御
• service.bootanim.exitの役割
service.bootanim.exitを参照しているのは、
frameworks/base/cmds/bootanimation/BootAnimation.cpp
0でない場合、即終了するようになっている。
そのため、開始時には「0」を設定していて、終了時には「1」を
設定している。
ブートアニメーションの制御
• ctl.start
ネイティブのサービスを制御する時、大事なのは「ctl.」で始ま
る値。
C、C++からはproperty_set、JavaからはSystemProperties.setを
使用して設定する。
property_set、SystemProperties.setのどちらから書き込みを
行った場合でも、initプロセスにメッセージが飛んで、書き込み
が行われる。
ブートアニメーションの制御
• ctl.start
initではプロパティを設定する時、先頭の4文字を確認する。
system/core/init/property_service.cpp
static void handle_property_set_fd()
{
...
if(memcmp(msg.name,"ctl.",4) == 0) {
// Keep the old close-socket-early behavior when handling
// ctl.* properties.
close(s);
if (check_control_mac_perms(msg.value, source_ctx)) {
handle_control_message((char*) msg.name + 4, (char*) msg.value);
...
}
ブートアニメーションの制御
• ctl.start
先頭の4文字が「ctl.」と一致する場合、それに続く文字列がsta
rtかstopかrestartかの確認を行う。
system/core/init/init.cpp
void handle_control_message(const char *msg, const char *arg)
{
if (!strcmp(msg,"start")) {
msg_start(arg);
} else if (!strcmp(msg,"stop")) {
msg_stop(arg);
} else if (!strcmp(msg,"restart")) {
msg_restart(arg);
} else {
ERROR("unknown control msg '%s'n", msg);
}
}
ブートアニメーションの制御
• bootanimationの終了
bootanimationの終了はctl.stopを使用せず、
service.bootanim.exitを見て、自分で終了している。
property_set("ctl.stop", "bootanim");を使用して終了させる
ことも可能。
Androidにサービスを追加する方法の
まとめ(Native編)
• サービスとして動かすプログラムを作成(C or C++)
• init.rcに登録
• 開始したいところでctl.startに値を設定
• 終了したいところでctl.stopに値を設定
Android Frameworkに独自のサービ
スを追加する方法
• Nativeで動作するサービスを追加する方法が分かったので、次は
Android Frameworkにサービスを追加する方法を知る。
• カスタムROM等、Androidを改造する際、Android Frameworkに独
自のサービスを追加して、アプリ等から使えると便利。
例.アプリからの使用は以下のようになる
IJcService jc = IJcService.Stub.asInterface(ServiceManager.g
etService("jcrom"));
try{
jc.changeJcromTheme();
}catch (Exception e) {
e.printStackTrace();
}
Android Frameworkに独自のサービ
スを追加する方法
• サービスの起動はどこで行われているのか?
サービスをどこで起動しているのか、見当が付かなければログを
眺めてみると良い。
Logcatを実行してAndroid起動時のログを眺めていると、以下の
ようなメッセージを確認することができる。
• ログを確認するとSystemServerが出していると分かる。
frameworks/base/services/java/com/android/server/SystemSer
ver.java
04-08 11:18:14.228 I/SystemServer( 1114): Package Manager
04-08 11:18:15.524 I/SystemServer( 1114): User Service
04-08 11:18:15.563 I/SystemServer( 1114): Reading configuration...
04-08 11:18:15.563 I/SystemServer( 1114): Scheduling Policy
Android Frameworkに独自のサービ
スを追加する方法
• SystemServerを見ると、以下のような処理が連続して見つかる。
独自のサービスの起動を追加する場合、それを参考にして追加す
ることにする。
Slog.i(TAG, "Scheduling Policy");
ServiceManager.addService("scheduling_policy", new SchedulingPolic
yService());
Android Frameworkに独自のサービ
スを追加する方法
• JcServiceを追加する例
• JcServiceを追加する場所はどこでも良いが、他のサービスの機
能を使う場合、使用するサービスの前に追加しないこと。
try {
Slog.i(TAG, "JcService");
ServiceManager.addService("jcrom", new JcService(context));
} catch (Throwable e) {
Slog.e(TAG, "Failure starting JcService", e);
}
Android Frameworkに独自のサービ
スを追加する方法
• 作成したソースコードは「frameworks/base/services」に置く。
accessibility
appwidget
backup
core
devicepolicy
java
jcrom
midi
net
print
restrictions
tests
usage
usb
voiceinteraction
AccoutManagerService
PackageManagerService
WindowManagerService
...
Android Frameworkに独自のサービ
スを追加する方法(まとめ)
• 今回作成したJcServiceはGithubにサンプルを上げています。
変更するAndroid.mk等、その他の細かい変更はそちらを確認する
と、一通り分かると思います。
Android Frameworkに独自のサービ
スを追加する方法(まとめ)
• 今回作成したJcServiceはGithubにサンプルを上げています。
変更するAndroid.mk等、その他の細かい変更はそちらを確認する
と、一通り分かると思います。
• が... まだGithubには無いので、来週上げたらこのスライドと共
にアップデートします。
JCROMの改造例④(見た目以外)
• バッテリーセーバーの機能拡張
バッテリーセーバーとは
• Lollipopから実装されている機能
• 電池残量が少なくなった時、以下のことを行う
– CPUのクロック周波数の最大値を下げる(機種によっては半
分以下)
– アニメーションを無効にする
– バックグラウンドでのデータ通信を制限する
– バイブレーションを無効にする
– その他、ディスプレイの輝度を下げたりGPSを切る等行う
バッテリーセーバーの余計な機能
この色は使う気がゼロになる
(個人的な感想です)
バッテリーセーバーの機能拡張
• JCROMでは次のことを出来るようにした
– CPUのクロック周波数の最大値を選択可能にする
– アニメーションを無効にするか選択可能にする
– バックグラウンドでのデータ通信の制限を選択可能にする
– バイブレーションを無効にするか選択可能にする
– ステータスバーとナビゲーションバーの色を変更するか選
択可能にする
JCROMの改造範囲
バッテリーセーバーの機能拡張で変更するところ
●
KitkatまではDalvik VM
●
Lollipop以降はART(Android
Run Time)
●
Application
設定項目を作る
●
Framework
アニメーション、バックグラウ
ンドデータ通信、バイブレー
ションのON/OFFが出来るように
変更する
●
HAL
CPUのクロック周波数の最大値
を設定出来るように変更する
バッテリーセーバーの機能拡張
• CPUのクロック周波数の最大値を選択出来るようにする
device/lge/hammerhead/power/power_hammerhead.c
#define LOW_POWER_MIN_FREQ "300000"
#define LOW_POWER_MAX_FREQ "729600"
#define NORMAL_MAX_FREQ "2265600"
sysfs_write(cpu_path_min[cpu], LOW_POWER_MIN_FREQ);
sysfs_write(cpu_path_max[cpu], LOW_POWER_MAX_FREQ);
バッテリーセーバーの機能拡張
• CPUのクロック周波数の最大値を選択出来るようにする
device/lge/hammerhead/power/power_hammerhead.c
#define LOW_POWER_MIN_FREQ "300000"
#define LOW_POWER_MAX_FREQ "729600"
#define NORMAL_MAX_FREQ "2265600"
static char *low_power_max_freq[] = {
LOW_POWER_MAX_FREQ,
"1267200",
"1574400",
"1728000",
NORMAL_MAX_FREQ
};
sysfs_write(cpu_path_min[cpu], LOW_POWER_MIN_FREQ);
sysfs_write(cpu_path_max[cpu], low_power_max_freq[select_num]);
バッテリーセーバーの機能拡張
• アニメーションを無効にするかの判定
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone
/PhoneStatusBar.java
private void checkBarMode(int mode, int windowState,
BarTransitions transitions, boolean noAnimation) {
final boolean powerSave = mBatteryController.isPowerSave();
final boolean anim = !noAnimation && mDeviceInteractive
&& windowState != WINDOW_STATE_HIDDEN && !powerSave;
if (powerSave && getBarState() == StatusBarState.SHADE) {
mode = MODE_WARNING;
}
transitions.transitionTo(mode, anim);
}
バッテリーセーバーの機能拡張
• ステータスバー、ナビゲーションバーの色を変更するかの判定
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone
/PhoneStatusBar.java
private void checkBarMode(int mode, int windowState,
BarTransitions transitions, boolean noAnimation) {
final boolean powerSave = mBatteryController.isPowerSave();
final boolean anim = !noAnimation && mDeviceInteractive
&& windowState != WINDOW_STATE_HIDDEN && !powerSave;
if (powerSave && getBarState() == StatusBarState.SHADE) {
mode = MODE_WARNING;
}
transitions.transitionTo(mode, anim);
}
バッテリーセーバーの機能拡張
• バイブレーションを無効にするかの判定
frameworks/base/services/core/java/com/android/server/VibratorService.java
mLowPowerMode = mPowerManagerInternal.getLowPowerModeEnabled();
private void startVibrationLocked(final Vibration vib) {
try {
if (mLowPowerMode && vib.mUsageHint !=
AudioAttributes.USAGE_NOTIFICATION_RINGTONE) {
return; //省電力ONだと即リターンする(バイブ無し)
}
//省略
}
バッテリーセーバーの機能拡張
• バックグラウンドでのデータ通信を制限するかの判定
frameworks/base/services/core/java/com/android/server/net/NetworkPolicyManage
rService.java
mPowerManagerInternal.registerLowPowerModeObserver(
new PowerManagerInternal.LowPowerModeListener() {
@Override
public void onLowPowerModeChanged(boolean enabled) {
synchronized (mRulesLock) {
if (mRestrictPower != enabled) {
mRestrictPower = enabled;
updateRulesForGlobalChangeLocked(true);
}
}
}
});
バッテリーセーバーの機能拡張
SELinuxの視点から見た
Android
SELinuxとは
• Androidで再会するまでのイメージ
SELinuxとは
• Android 5.0以降の現実
Android Compatibility Definition Document(CDD)で「MUST」
AndroidにおけるSELinuxを知る
• SELinuxは許可する操作、禁止する操作の定義(セキュリティポ
リシー)に基づいて動作する
SELinuxを使用しない場合
Process A Linux Kernel
File A
File B
File C
-rw-r--r–- hoge hoge File A
-rw-r---–- hoge hoge File B
-rw------– hoge hoge File C
sola
solaは管理者権限を持たない一般ユーザ
hogeのグループには属していない
solaがProcess Aを実行してFile A, File B, File Cにアクセス(read)す
る例。
SELinuxを使用しない場合
Process A Linux Kernel
File A
File B
File C
-rw-r--r–- hoge hoge File A
-rw-r---–- hoge hoge File B
-rw------– hoge hoge File C
sola
solaは管理者権限を持たない一般ユーザ
hogeのグループには属していない
乗っ取られてProcess Aがroot権限を持ってしまった時は、
File A, File B, File C全てにアクセスされる。
乗っ取り
SELinuxを使用した場合
Process A
Linux Kernel
File A
File B
File C
-rw-r--r–- hoge hoge File A
-rw-r---–- hoge hoge File B
-rw------– hoge hoge File C
sola
solaは管理者権限を持たない一般ユーザ
hogeのグループには属していない
SELinuxを使用するとパーミッションによるアクセス制御の他に、
セキュリティポリシーによる制御が行われる。
SELinux
Security
policy
File B, File Cはrootで
あってもアクセス出来な
いようにSecurity
policyを定義する
SELinuxを使用した場合
Process A
Linux Kernel
File A
File B
File C
-rw-r--r–- hoge hoge File A
-rw-r---–- hoge hoge File B
-rw------– hoge hoge File C
sola
solaは管理者権限を持たない一般ユーザ
hogeのグループには属していない
乗っ取られてProcess Aがroot権限を持ってしまっても
Security policyの定義に従うので、File BやFile Cを守れる。
SELinux
Security
policy
File B, File Cはrootで
あってもアクセス出来な
いようにSecurity
policyを定義する
乗っ取り
AndroidにおけるSELinuxを知る
• SELinuxは許可する操作、禁止する操作の定義(セキュリティポ
リシー)に基づいて動作する
• AOSPで公開されているセキュリティポリシーを読むと、全体の仕
様が見えてくる(ただし全て読むのは大変疲れる)
SELinuxの範囲
セキュリティポリシーを定義する人が見るべきところ
●
セキュリティポリシーはアプリ
ケーションからデバイスドライ
バまで様々な定義が存在する
AOSPのセキュリティポリシー
• AOSPのセキュリティポリシーはexternal/sepolicyに入っている
(おそらく次のNからはsystem/sepolicyに移動)
AndroidにおけるSELinuxを知る
• SELinuxは許可する操作、禁止する操作の定義(セキュリティポ
リシー)に基づいて動作する
• AOSPで公開されているセキュリティポリシーを読むと、全体の仕
様が見えてくる(ただし全て読むのは大変疲れる)
• 最初は気になったサービスやアプリのセキュリティポリシーを読
んで、確認してみる
セキュリティポリシーの例
• bootanimationのものを見てみる(bootanim.te)
# bootanimation oneshot service
type bootanim, domain;
type bootanim_exec, exec_type, file_type;
init_daemon_domain(bootanim)
binder_use(bootanim)
binder_call(bootanim, surfaceflinger)
allow bootanim gpu_device:chr_file rw_file_perms;
allow bootanim oemfs:dir search;
allow bootanim oemfs:file r_file_perms;
allow bootanim audio_device:dir r_dir_perms;
allow bootanim audio_device:chr_file rw_file_perms;
allow bootanim surfaceflinger_service:service_manager find;
AndroidにおけるSELinuxを知る
• SELinuxは許可する操作、禁止する操作の定義(セキュリティポ
リシー)に基づいて動作する
• AOSPで公開されているセキュリティポリシーを読むと、全体の仕
様が見えてくる(ただし全て読むのは大変疲れる)
• 最初は気になったサービスやアプリのセキュリティポリシーを読
んで、確認してみる
• 次に自分で作成したサービスやアプリ用にセキュリティポリシー
を作成してみる
実は...
●
Android Frameworkに追加したJcService、実はセキュリティポ
リシーの定義を追加しないと動きません
セキュリティポリシーの例
• ログを確認すると、起動時にエラーとなっていたことが分かる
E/SELinux ( 191): avc: denied { add } for service=jcrom scontext=u:r:system_server:s0 tcontext=
u:object_r:default_android_service:s0 tclass=service_manager
E/ServiceManager( 191): add_service('jcrom',2b) uid=1000 - PERMISSION DENIED
E/SystemServer( 820): Failure starting JcService
E/SystemServer( 820): java.lang.SecurityException
E/SystemServer( 820): at android.os.BinderProxy.transactNative(Native Method)
E/SystemServer( 820): at android.os.BinderProxy.transact(Binder.java:503)
E/SystemServer( 820): at android.os.ServiceManagerProxy.addService(ServiceManagerNative.java:150)
E/SystemServer( 820): at android.os.ServiceManager.addService(ServiceManager.java:72)
E/SystemServer( 820): at com.android.server.SystemServer.startOtherServices(SystemServer.java:539)
E/SystemServer( 820): at com.android.server.SystemServer.run(SystemServer.java:272)
E/SystemServer( 820): at com.android.server.SystemServer.main(SystemServer.java:170)
E/SystemServer( 820): at java.lang.reflect.Method.invoke(Native Method)
E/SystemServer( 820): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.jav
a:726)
E/SystemServer( 820): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
セキュリティポリシーの追加
• service_contexts
diff --git a/service_contexts b/service_contexts
index 85dcd3d..9a3d07f 100644
--- a/service_contexts
+++ b/service_contexts
@@ -56,6 +56,7 @@ isms_msim u:object_r:radio_service:s0
isms2 u:object_r:radio_service:s0
isms u:object_r:radio_service:s0
isub u:object_r:radio_service:s0
+jcrom u:object_r:jcrom_service:s0
jobscheduler u:object_r:jobscheduler_service:s0
launcherapps u:object_r:launcherapps_service:s0
location u:object_r:location_service:s0
セキュリティポリシーの追加
• service.te
diff --git a/service.te b/service.te
index 56478d0..df672a0 100644
--- a/service.te
+++ b/service.te
@@ -102,3 +102,4 @@ type wifip2p_service, app_api_service, system_server_service, service_m
anager_ty
type wifiscanner_service, system_api_service, system_server_service, service_manager_type;
type wifi_service, app_api_service, system_server_service, service_manager_type;
type window_service, system_api_service, system_server_service, service_manager_type;
+type jcrom_service, app_api_service, system_server_service, service_manager_type;
AndroidにおけるSELinuxを知る
• SELinuxは許可する操作、禁止する操作の定義(セキュリティポ
リシー)に基づいて動作する
• AOSPで公開されているセキュリティポリシーを読むと、全体の仕
様が見えてくる(ただし全て読むのは大変疲れる)
• 最初は気になったサービスやアプリのセキュリティポリシーを読
んで、確認してみる
• 次に自分で作成したサービスやアプリ用にセキュリティポリシー
を作成してみる
• 地道にセキュリティポリシーを読み書きしていく中で、SELinux
の視点から見たAndroidが見えてくると思います。
補足
●
JCROMやSELinuxは同人誌でも解説を書いてます。
電子版がBOOTHにあるので、興味あれば読んでみて下さい。
Android Internals
https://booth.pm/ja/items/178226
JCROMの解説
https://booth.pm/ja/items/65895
以上です。
ありがとうございました。

More Related Content

カスタムROM開発者の視点から見たAndroid