eSIM が無ければ Pixel 4 を潰せばいいじゃない
リムーバブルな eSIM が手に入らない
eSIM にコマンドを送りたくなること、ありますよね。4FF/nano カードタイプのいわゆるリムーバブルな eSIM を入手し、PC/SC インターフェースを通して ES10x のコマンドを送ることができればと考えたんですけど、そもそもそんな eSIM を簡単に入手できそうにありません。
SGP.26 のテスト証明書を入れた商品は見つけましたが、それでは今後やれることが限られてしまいそうです。海外の eSIM 事業者さんにもお尋ねしてみましたが、門前払いみたいな感じでした。まぁ、そりゃそうでしょう。
リムーバブルじゃなくてもいいか(諦めた)
全く eSIM が手に入らないのかというと、そういうわけではありません。リムーバブルなカードタイプではなく、更には既に商品に組み込まれてしまっているだけのことです。所望のものが手に入らないのはもう仕方がないということで、Pixel 4 に組み込まれている eSIM を利用することにしました。
我ながら適当なワークアラウンドを考えたものです。adb shell content コマンドを使って ES10x コマンド部を Android 端末に渡し、端末内に追加する Content Provider を通して eSIM に送り込みます。アプリから見れば、ES10x コマンドの送信をお願いした先の仕組みが違うだけのことで、何ら不都合はありません。不都合として何か挙げるとするならば、Pixel 4 をルート化しないといけないことくらいでしょうか。
今回作成した Content Provider はこちら
クライアントから ES10x コマンドを受け取り、APDU ヘッダを付けて STORE DATA コマンドにして、テレフォニー API を使って eSIM に送りこむだけの Content Provider です。論理チャネルのオープン・クローズや ISD-R の選択もやりますが、どうあれ大した処理ではないのでコード量は極小です。
ProfileInfoListRequest 'BF2D' の実行例をご紹介します。Content URI の最後のスラッシュ以降が ES10b コマンド部で、そこで指定したコマンドデータが STORE DATA コマンドに載せられて eSIM まで運ばれます。要求するデータオブジェクトは profileState '9F70' と profileName '92'、profileClass '95'、notificationConfigurationInfo 'B6' の四つ。
$ adb shell content query --uri content://com.github.cheeriotb.isdrap.provider/store/BF2D075C059F709295B6
Row: 0 response=BF2D7DA07BE31D9F7001009214416E726974737520546573742050726F66696C65950100E35A9F700100920349494A950102B64C302480020410811E534D2D56342D3033332D412D47544D2E50522E474F2D4553494D2E434F4D302480020780811E534D2D56342D3033332D412D47544D2E50522E474F2D4553494D2E434F4D9000
Android 端末からは、ProfileInfoListResponse 'BF2D' とステータスワード '9000' が返っています。テストプロファイルがひとつ、そして IIJ の商用プロファイルがひとつ入っていることが読み取れます。
ProfileInfo | profileState | disabled (0) | |
---|---|---|---|
profileName | Anritsu Test Profile | ||
profileClass | test (0) | ||
ProfileInfo | profileState | disabled (0) | |
profileName | IIJ | ||
profileClass | operational (2) | ||
notificationConfigurationInfo | profileManagementOperation | notificationDelete (3) | |
notificationAddress | SM-V4-033-A-GTM.PR.GO-ESIM.COM | ||
notificationConfigurationInfo | profileManagementOperation | notificationInstall (0) | |
notificationAddress | SM-V4-033-A-GTM.PR.GO-ESIM.COM |
端末から返ってくるデータサイズが大きい場合の動作をまだ確認できていないので、うまく動かないケースがあれば都度修正しようと思います。また、現状は Pixel 端末を想定したコードになっていますが、ほんの少し書き換えるだけで Rakuten Mini 等にも転用できるはずです。
久しぶりにリフレクションしました
今回利用した API の中には @hide なものも含まれていたので、久しぶりにリフレクションを使いました。Kotlin だと、他にもっと綺麗に書く方法もあるんでしょうか。
transmit = TelephonyManager::class.java.getDeclaredMethod( "iccTransmitApduLogicalChannelBySlot", Int::class.java, Int::class.java, Int::class.java, Int::class.java, Int::class.java, Int::class.java, Int::class.java, String::class.java) val tm = context!!.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager? ?: throw UnsupportedOperationException("Telephony Manager is unavailable") transmitRsp = Response(transmit.invoke(tm, PHYSICAL_SLOT_NUM, openRsp.channel, STORE_DATA_CLA, STORE_DATA_INS, p1, seqNumber, block.length / 2, block) as String?)
あと、サービスだけどうして Java で書いちゃったのかは謎です。適当に試してみたときに、勝手に手がそう動いちゃったんだと思います。