Android 4.0.4が入ったPanasonic TOUGHPAD JT-B1を、Alpine Linuxで起動するSpotifyプレーヤーにした。 純正カーネルを残してユーザー空間を入れ替え、画面、タッチ、5 GHz Wi-Fi、音声出力を動かし、日本語入力付きの縦画面GUIを作った。 内蔵LTEモデムのSIMロックも解除し、povo 2.0での通信を確認した。
最初の目的は「Androidの中でLinuxを動かす」ことではなく、端末の通常起動先をLinuxへ置き換えることだった。 途中から用途をSpotifyに絞り、端末自身で曲を探して再生できるところまで進めた。 この記事は、2026年11月4日から6日を中心とするセッション履歴、作業メモ、Triliumの記録、保存した検証ログをもとに再構成している。 調査にはコーディングエージェントを使い、画面の目視、実際のタッチ、スピーカーとイヤホンの聴取は手元の実機で確認した。
これは所有端末での改造記録であり、同型機にそのまま適用するインストール手順ではない。 ストレージとモデムの書き換えには起動不能になる危険がある。 全体バックアップは取得したが、分解せずに復旧できる独立した手段と、全体イメージの実機復元は未検証である。
公開コードと試せる範囲
常用したコードを整理して、chun37/jt-b1-linuxで公開した。 日本語GUI、音声監視、Wi-Fi互換処理、povo接続サービスに加え、実機なしで動くテストと画面プレビューを収録している。 試す場合は、リポジトリのREADMEにあるPC向け手順から始められる。
公開版は、すでにLinuxを起動できる端末向けのソースコード集であり、Androidからの一括インストーラーではない。 root取得、rawパーティション書き込み、SIM解除とモデムFW書き込みの試行は含めていない。 メーカーのバイナリー、認証情報、個体情報も同梱せず、Spotifyは読者自身のClient IDと認証情報を使う。 公開用に設定と補助ツールを整理した版はPCで検証しているが、その変更を含む版を実機へ再配置した試験はしていない。
出発点となったJT-B1
対象は日本向けのJT-B1APAAABJで、Androidのビルドは09.0101だった。 実機から取得した情報は次のとおり。
| 項目 | 確認した内容 |
|---|---|
| SoC | TI OMAP4460 ES1.1、ARMv7。SoC種別の表示はHS |
| OSとカーネル | Android 4.0.4、Linux 3.0.8+ |
| メモリーとストレージ | Linuxから見えるRAMは約658 MiB、eMMC全体は15,634,268,160バイト |
| 画面と入力 | 1024×600のomapfb、Atmel maXTouch、A1〜A3と電源の物理ボタン |
| 通信と音声 | wl12xx系Wi-Fi、Sierra Wireless MC7700、TWL6040系音声、3.5 mm端子 |
通常のADB shellはUID 2000で、adb rootは製品ビルドとして拒否された。
adb reboot bootloaderを試してもfastboot端末は現れず、通常のAndroidへ戻った。
ただし、この一度の結果だけでfastboot機能が存在しないとは断定できない。
作業には「分解しない」「UARTを使わない」という制約があった。 そのため、ブートローダーへシリアル接続して設定する方法ではなく、USB ADBと保存した純正ソフトウェアから起動経路を探した。
純正更新パッケージから起動処理を調べる
ZIPの中にそのままカーネルがあるわけではなかった
最初にPanasonicの公開サポートから、国内向け09.0096の更新パッケージを取得した。 後には海外向けの7版も保存し、起動処理の違いを調べた。
更新ファイルは、拡張子がdatのZIPにallやmtd6、mtd13などが入った構造だった。
ただし、mtd6をそのまま端末の6番目のパーティションへ書けばよい、という意味ではない。
アーカイブ内の論理名と実機のパーティション番号は分けて扱った。
更新処理を担当するARM実行ファイルのUpdateAgentを抽出すると、RedBend vRMの更新エンジンが含まれていた。 そこで、実機で更新を走らせる代わりに、Unicorn(CPU命令をエミュレーションするライブラリー)で更新エンジンの一部を実行した。 ゲストが見るファイルとブロックデバイスはPythonのメモリー内に置き、実機やホストのデバイスへアクセスさせない構成にした。
この方法で通常起動用とリカバリー用のイメージを復元し、uImageのヘッダーと本体のCRC一致を確認できた。
uImageの表示名はLinux-2.6.35だったが、展開したカーネルの実体は3.0.8+だった。
ファイル名やヘッダーの表示名だけでは、実際の版は判断できなかった。
署名らしい領域と、起動できることは別だった
復元したイメージにはCertISWという外側のヘッダーがあり、uImageに対するSHA-1や署名情報らしい領域があった。
SHA-1とCRCは照合できたが、暗号署名の方式や、ブートローダーがどの段階で何を強制検証するかまでは確定できなかった。
SoCがHS品と報告されることも、無署名カーネルを簡単に起動できる前提にはできない理由だった。
リカバリーの鍵も調べた。
testkey.x509.pemという名前の証明書は、AOSPで公開されているtestkeyではなくPanasonicの証明書だった。
標準OTAの署名検証とメーカー独自のSD更新処理にも別の制御フローがあり、一方の調査結果を他方の結論へ流用できなかった。
結局、署名付きカーネルそのものを改造する方法は採らなかった。 以後は、純正カーネルのままLinuxのユーザー空間を起動できないかを調べた。
kexecとリカバリーも近道にはならなかった
純正カーネルにはkexecに関係する名前があった。
しかし保存バイナリーを調べると、対象の経路は通常の別カーネルへの引き渡しではなく、メーカー独自のダンプと再起動処理につながっていた。
シンボルが存在するだけで、一般的なkexec起動が使えるとは判断できなかった。
adb reboot recoveryでリカバリーメニューへ入ることはできた。
そこには外部SDからの更新やMC7700のファームウェア更新があったが、ADBはofflineで、保存した旧版の起動定義ではadbd自体がコメントアウトされていた。
Androidが起動しなくなったときに物理キーだけで確実に入れる手順も確認できなかった。
この時点で「リカバリーがあるから、失敗しても戻せる」とは言えなかった。
rootを取っても、すべてにアクセスできるわけではない
COW競合による一時変更とCCSecurity
初期の試行では、Dirty COW系の手法で、コピーオンライト(COW)の競合を利用した読み取り専用マッピング経由のファイル変更が成立した。
続いてrun-asの一部を一時的に変更し、実効UID 0で動く固定の診断コードを実行できた。
変更した実行ファイルは試行後に元へ戻し、ファイル全体の一致を確認した。
ところが、実効UIDが0でもブロックデバイス、セキュリティポリシー、シェルの起動は拒否された。
カーネルの起動引数にはccsecurity=onがあり、TOMOYO系のCCSecurityが動いていた。
UnixのUIDだけではなく、どのプログラムから実行されたかというドメインにも制限があった。
このため「root化成功」と「自由なrootシェルを取得できた」は分けて記録した。 限定したコードで設定RAMや起動定義を読めても、そのままOSを置き換えられるわけではなかった。
内部ストレージのスクリプトを起動するLiNSという仕組みも調べた。
有効化フラグは0だったが、設定ライブラリーはRAMだけでなく保存領域も更新する構造で、単純な一時変更として扱えなかった。
さらに、保存したポリシーにはchrootやpivot_rootへの制約があり、LiNSを有効にすればLinuxへ切り替えられるという結論には至らなかった。
この経路は最終的な起動方法には使っていない。
管理デーモンのコマンド処理から一時rootへ
別の経路として、メーカーの端末管理サービスを解析した。 管理APIの呼び出しがrootの管理デーモンを起動し、そのデーモンが受け取った文字列をシェルコマンドへ埋め込む処理があった。 ここにはシェル用のエスケープがなかった。
デーモンの生存時間は短く、通常の管理クライアントより先に接続する必要があった。 接続の競合を制御し、メーカー設定を変更する本来のコマンド部分を実行させない形にして、rootのコマンド実行を確認した。 これにより、通常のshellでは見えないディレクトリーの読み出しもできた。
検証中はWi-Fiなどの非loopbackインターフェースを停止した。 デーモンが全インターフェースで待ち受ける構造だったため、管理機能をネットワークへ公開しないためである。 処理後にはデーモンの停止と管理用プロパティーの復元を確認した。
なお、純正の監査デーモンには、rootに関する監査ログを契機としてメーカー設定を保存する処理も見つかった。 「読み取りコードしか実行していない」ことは、システム全体に書き込みが一切発生しない保証にはならなかった。 過去の試行で実際にその保存が発生したかは確認していない。
Androidを保存し、Alpineを直接起動する
全体バックアップと、その限界
OSを書き込む前に、eMMC全体と各2 MiBのboot0、boot1を保存した。 全eMMCは約15.6 GBで、転送元と受信側、PCでの再読み出しのSHA-256が一致した。
ただし、直接のデバイスパスはrootからでも拒否された。
通常再起動後、/dev配下に作った一時的なブロックデバイスノードから読み出す方法が成立した。
同じ領域でもパスによって結果が違い、UID 0だけでは説明できない制限がここにも現れた。
バックアップはAndroid稼働中のコピーである。
/dataを安定した読み取り専用状態へ移す追加の試行は失敗し、その条件での転送は始めなかった。
元イメージを保全したうえで、PC上のuserdataの別コピーだけを修復し、fsckの再検査が正常終了することは確認した。
ハッシュが一致したバックアップの取得と、アプリケーションの整合性や実機への復元成功は別である。 この制限を残したまま、Linuxの書き込みと起動試験へ進んだ。
純正カーネルから別のrootfsへ
採用したのは、ブートローダーと純正カーネルを維持し、起動引数でAlpineのrootfsを指定する構成だった。
rootfsは、OSが/として使うファイルシステムである。
| 領域 | 用途と変更 |
|---|---|
| p22 | 元のfotadelta領域をAlpine Linux 3.23.7 armv7のrootfsへ転用 |
| p4 | 起動環境のbootargsをLinux用へ変更 |
| ブートローダーとカーネル | 変更せず純正を利用 |
| それ以外のAndroid領域 | このOS導入では初期化せず、Linuxからもマウントしない |
起動引数の要点は次のとおり。 これは実機に導入した設定の抜粋であり、書き込みコマンドではない。
root=/dev/mmcblk0p22 rootwait rdinit=/jtb1-no-ramdisk-init init=/sbin/init ccsecurity=off
rdinitには存在しないパスを指定した。
既存initramfsのAndroid用/initへ進まず、カーネルが外部ルートをマウントして/sbin/initを実行する経路を使うためである。
この挙動は保存したバイナリーで事前に調べ、最終的には実機起動で確認した。
書き込みには、対象のデバイス番号、開始位置、容量、非マウント状態、入力イメージのサイズとハッシュを固定検査する小さなARMプログラムを使った。
書き込み後はfsync、キャッシュ破棄、領域全体の読み戻しを行い、SHA-256を照合した。
初回用の起動処理には、LinuxのrcSまで到達したら起動環境をいったん元のAndroid設定へ戻す処理を入れた。 USBの初期化に失敗しても、次回の起動先を戻しておく意図だった。 ただし、rcSより前に失敗した場合は役に立たず、元設定からのAndroid再起動も試していない。 独立したリカバリーの代わりにはならない。
chrootではなくLinuxがPID 1になった
初回起動後、USB ADBはJT-B1-LINUXとして現れた。
PID 1の実体は/bin/busybox、コマンドラインは/sbin/initで、/はp22のext4を直接マウントしていた。
Androidのzygote、system_server、surfaceflingerは存在しなかった。
この確認後にLinux用の起動設定を確定し、もう一度再起動した。 次の起動でも同じ構成になったため、Android上のchrootではなく、通常起動するユーザー空間の置き換えが成立した。
最初はUSBのCLIだけだった。 画面もタッチもWi-Fiも、自動的にAndroidと同じように動くわけではなかった。
黒い画面を表示させ、電池LEDを戻す
フレームバッファーの内容と実際の表示は違う
Alpine起動後、画面は黒く、バックライトが薄く光るだけだった。
/dev/fb0に画像を書けて、読み戻した画素が一致しても、実際のパネルには何も出なかった。
表示コントローラーの状態を調べると、ソフトウェア上ではoverlayが有効でも、実レジスターでは表示先アドレスとサイズが0で、有効ビットも落ちていた。
同じオフセットへのFBIOPAN_DISPLAYは、純正ドライバー内で設定の反映を省略していた。
解決したのは、現在の解像度を変えず、FBIOPUT_VSCREENINFOをNOW | FORCEで実行することだった。
これで実際の表示設定が反映され、Alpineの文字が画面に現れた。
以後は起動処理にこの初期化を組み込んだ。
当初はAndroidからデバイスツリーを取り出す案もあった。 しかし今回の構成ではDTBを抽出して新カーネルへ移植したのではなく、純正カーネルに組み込まれた機種設定と既存ドライバーを使っている。
LEDは常時点灯ではなく、純正の状態表示へ
電源LEDも、単に緑で固定するのではなく、Android時代と同じ状態表示にしたかった。 純正のframeworkとライトHALを調べ、電池状態からLEDへの対応を取り出した。
| 電池状態 | 移植した表示 |
|---|---|
| 過熱 | 橙色で点滅 |
| 非充電で15%未満 | 赤色で点灯 |
| 満充電 | 緑色で点灯 |
| 充電中 | 橙色で点灯 |
| その他 | 消灯 |
最初の監視処理は「電池の状態が変わったときだけLEDを更新」する実装だった。 ところが再起動後、電池状態が同じままLEDの実値だけが消灯側へ変わり、更新されなかった。 状態の変化だけでなくLEDの現在値も照合し、不一致を再適用する方式へ直した。 満充電時の緑点灯は、再起動後も目視で確認した。
この過程には失敗もあった。
メーカー設定用のcfgdrvとddsyncを使う一時試験の直後にADB shellが開けなくなり、強制オフからの再起動で復帰した。
LEDのtrigger変更でも処理が戻らなくなる試行があり、これらを常用設定には採用しなかった。
表示とLEDは、それらの試行を繰り返さずに動く構成へ切り替えた。
5 GHz Wi-Fiとタッチを動かす
5 GHzが見えない原因は地域設定だった
Wi-Fiは、元のAndroidから保存したcompat、cfg80211、mac80211、wl12xx、wl12xx_sdioと対応ファームウェアを再利用した。
実行中の純正カーネルとモジュールのvermagicが一致することを確認している。
最初は目的のアクセスポイントを検出できなかった。 Androidでは見えていた5 GHzのネットワークが、Alpineでは見えなかった。
原因は、regulatory domain(無線の地域別利用条件)の初期要求が未処理だったことだった。 JPを要求するだけではエラーになり、先にworldの初期要求へ応答する必要があった。 初期要求を処理してからJPを設定し、公開の無線規則の保守的な部分集合を返す補助プログラムを作った。 送信出力、passive scan、DFSなどの制約は残し、利用可能な周波数を無制限に広げる方法は採っていない。
別の問題として、新しいwpa_supplicantが送るscan flagsを古いnl80211が拒否した。
該当プロセスだけでこの属性を付けない互換処理を追加した。
これで5 GHzの5180 MHz、チャンネル36に接続し、DHCP、外部への通信、HTTPS取得が成功した。 通常再起動後の自動接続と、画面のIP表示も確認した。 今回確認したのは5 GHzであり、6 GHz対応を確認したわけではない。
タッチは純正モジュールを再利用できた
タッチには純正のatmel_mxt_ts.koを使った。
Alpineでは未ロードだったため、配置してロードすると入力イベントが現れた。
座標範囲はXが0〜1023、Yが0〜599だった。
まず接触位置に水色の十字を描く小さなプログラムを作り、押す、動かす、離すという入力を確認した。 実際に触った位置と十字の位置が一致し、再起動後も同じように動いた。
メーカー設定ファイルの一部は取得できていなかったが、コントローラーに残る設定で入力は動いた。 ここでも、設定用モジュールを追加ロードするより、動いているドライバーと入力をそのまま使う方を選んだ。
Spotify Soloistを古いLinuxで動かす
汎用モバイル環境より、用途を絞った構成へ
GUIを考え始めた時点では、Plasma MobileやUbuntu Touchのような環境も候補だった。
しかし、古いカーネル、約658 MiBの可視RAM、未成立の現代的な描画基盤という制約があった。
/dev/driはなく、代わりにfb0とPowerVR系のデバイスが見えていた。
GPUが存在しないのではなく、現行のWayland環境をそのまま使える状態ではなかった。
そこで用途をSpotifyへ絞った。 別機器の再生を操作するリモコンではなく、JT-B1自身から音を出すことを目標にした。
再生側にはSpotify公式のSoloistを使った。 調査時点の公式資料にARMv7 Linux対応とロスレス対応の記載があり、ローカルAPIも備わっていた。 採用したビルドは1.3.10.42である。
Alpineのmuslと、Soloistのglibcを分ける
公式ARM32バイナリーはglibc向けだった。 Alpineのmuslをシステム全体で置き換えるのではなく、Debian由来のglibc 2.41と必要なライブラリーを専用ディレクトリーへ置いた。 専用ローダーからSoloistを起動し、同様にPulseAudioも必要な依存関係だけを持つ構成にした。
起動できても、音声出力には次の問題があった。
新しいDebianの標準libasoundはtime64のioctlを使い、古いカーネルでSNDRV_PCM_IOCTL_SYNC_PTR failed (-25)になった。
ALSA 1.2.14を専用sysrootでクロスビルドし、-D_TIME_BITS=32 --disable-year2038で旧来の互換経路を使う私有版へ差し替えた。
システムのALSAは変更していない。
この選択には2038年以降の時刻対応を保証しないという制限がある。
最終的な音声用ディレクトリーは約44 MiBになった。 SoloistのWebSocketはloopbackだけ、PulseAudioはroot専用のUnixソケットだけで待ち受ける構成にした。
playingなのに無音だった理由
Soloistがplayingを返し、ALSAのPCMがRUNNINGになっても、最初はスピーカーから何も聞こえなかった。
PCMのポインターは進み、再生側のサンプルにも信号があった。
ソフトウェアが音声データを送っていることと、物理出力が有効なことは別だった。
純正の音声HALとカーネルを調べると、通常のHandsfree Playback Volumeは、内部フラグが無効なときに希望値だけを保持する実装だった。
ミキサーの表示値が変わっても、コーデックのゲイン設定は変わっていなかった。
別にあるAlt Handsfree Playback VolumeのVolume for MultiMediaが、音楽再生用のゲインを実際に反映する経路だった。
保存カーネルの該当処理をPC上で限定実行して動作を確認し、実機では再生を止め、音量を下げてから適用した。
その後、内蔵スピーカーから音が出た。
3.5 mm端子も別途対応した。 こちらはヘッドセット側のDL1経路と、再生用のSidetone Mixer設定が必要だった。 スピーカーとイヤホンの双方で、実際の発音を確認できた。
ロスレスの表示と、受信形式の検証を分ける
最高品質を要求する設定を入れ、公式Spotify側でロスレス表示が出ることも確認した。 ただし、受信したストリームのコーデックとビット深度を独立に検査したわけではない。
確認したALSA出力はS32_LE、2チャンネル、44.1 kHzだった。 このPCM形式だけから、配信元のストリームがFLACだったと結論づけることはできない。 「ロスレス表示を確認した」と「配信から出力までを独立監査した」は区別している。
日本語で曲を探せる縦画面GUIを作る
再生APIと検索APIを分担する
Soloistだけでは、検索や保存曲の一覧を取得するAPIが足りなかった。 そのため、再生はSoloist、検索やライブラリーの取得はSpotify Web APIという分担にした。
自作タッチGUI
├─ Spotify Web API:検索、保存曲、アルバム、プレイリスト
└─ SoloistのローカルAPI:再生、停止、曲送り、音量
└─ PulseAudio → 私有ALSA → 純正音声ドライバー
Web APIの認証は、PCのloopback上でPKCEを使って行った。 Client Secretを端末や会話へ渡さずに認可し、端末側にはトークンの更新処理も実装した。 古いADBのshellはPTYを経由するため、秘密をそのまま標準入力へ流す方法は避け、非秘密データで転送と改ざん拒否を検査してから、暗号文だけを転送した。
フレームバッファーへ直接描く
GUIには専用のPythonとPillow、Noto CJKフォントを使った。 画面は論理600×1024の縦向きで、物理1024×600のフレームバッファーへソフトウェア回転して出力する。 タッチ入力も逆変換する。 XorgやWaylandのデスクトップ環境は導入していない。
検索には日本語キーボードとSKKの漢字候補を用意した。 検索結果、保存曲、アルバム、プレイリストを表示し、ページ送りと再生操作を接続した。 実機では日本語入力から検索、選曲、再生、停止までを検査し、手元でも好きな曲を検索して再生できた。
プレイリストは、調査時点のWeb APIのDevelopment Mode制限を受ける。 所有するプレイリストの非空の内容は取得できたが、他人のプレイリストを含む公式Androidアプリの全機能を再現したわけではない。 曲行からの単曲再生と、アルバムやプレイリスト全体のcontext再生にも違いがある。
表示だけの試作から始め、実API、トークン更新、入力、再生操作を順に接続した。 画面が出た段階と、端末単独で使える段階を分けて進めた。
表示プロセスが落ちても、音声操作を放置しない
GUIには監督プロセスを置いた。 表示子プロセスの終了を検出し、所有する操作用の子プロセスをPIDだけでなく開始時刻なども照合して回収する。 再生状態を変更する操作が途中だった場合は、一時停止する処理も入れた。
通常終了、表示子の強制終了、未完了の操作を持つ表示子の強制終了を実機で試した。 画面の復旧と、必要な場合の操作子の停止、一時停止を確認した。 監督自身を含むすべての強制終了タイミングを保証するものではない。
携帯回線はSIMロック解除とモデム復旧が必要だった
PH-NET PINは通常のSIM PINではなかった
内蔵のMC7700は、保存した純正Sierra系シリアルドライバーとGobiNetをロードすると認識した。
ところがpovoのSIMを入れても、AT+CPIN?はPH-NET PIN、PNロックの照会は有効という結果だった。
これはSIM自体のPIN入力待ちではなく、モデムのネットワーク個人化ロックだった。 APNを書くだけで接続できる状態ではなかった。 メーカーとdocomoにはサポート対象外と案内され、無料で解除できる手段を調べることになった。
ファームウェアの試行で通常応答を失った
解除調査は最初から成功したわけではない。 途中のファームウェア書き込みやNVM操作を経た後、モデムが通常のATとQMIに応答せず、ダウンロード系の単一ポートだけになる状態に入った。 どの操作が原因だったかを、一つのログから確定することはできなかった。
復旧では、それまでの成功らしい表示をいったん根拠から外した。
stockというファイル名でも元のファームウェアとは限らず、過去のATブリッジにはATIとOKを合成する実装もあった。
その応答は、実モデムが正常である証拠には使えなかった。
公式DocomoパッケージのAPPL部分と別取得した03.05.19.00AのAPPLが完全一致することを確認し、BOOTを追加で書かずにAPPLだけを書き込んだ。 更新ツールは全チャンク転送後の終了処理でタイムアウトし、正常終了にはならなかった。 しかし、その後に実機のシリアルポートへ直接接続するとATとQMIが復帰し、モデムの電源を入れ直しても応答した。
復旧したのは、以前に動作ログのある03.05.19.00Aである。 最初に記録した03.05.19.00とバイト単位で同じ工場状態へ戻した、という意味ではない。
開発用認証とSIM解除は別だった
開発用のOPENLOCKやOPENMEP認証がOKになっても、PNロックは残った。
認証成功をSIMロック解除成功とは扱えなかった。
EFSにある個人化情報の読み出しも拒否され、この方法では解除キーを得られなかった。
有料候補としてDC-unlockerも調べた。 WineではCOMポートを認識せず、WindowsへUSB/IPで転送した環境ではMC7700の検出まで確認した。 これは接続適合の確認であり、有料解除を実行したわけではない。 最終的に購入はしていない。
旧Qualcommの診断実装から正しいキーを読む
成功につながったのは、公開されている旧QualcommのGSDI診断実装だった。 EFSファイルを直接読む方法とは別に、遅延応答を返すgeneric診断に個人化キーを読み出す処理があった。 対象のMC7700でこの形式の応答を検査し、端末内に保持される正しいNW解除キーを取得できた。
取得した端末識別情報を直接のAT照会と照合し、正しい解除キーでPN解除を一度だけ実行した。
直後の照会を期待どおりに受け取れずラッパーの検査は失敗したが、解除処理を繰り返さず、独立したATとQMIで状態を確認した。
最終的にCPIN READY、PNロック無効、QMIのPNがdeactivatedになった。
IMEIや解除キーは公開用のログに出していない。 外部の解除サービスへ識別情報を送ったり、推測コードを総当たりしたりせずに解除できた。 この結果は対象モデムとファームウェアでの実績であり、別機種への適用を保証しない。
Wi-Fiに迂回していないことを確かめる
解除後、APNをpovo.jpに設定し、KDDIのLTE Band 1へ登録した。
データ接続にはGobiNet経由でQMIのWDSを使い、接続時にIP、ゲートウェイ、DNSを取得する常用サービスを作った。
標準のqmicliはこのqcqmiデバイスでそのまま使えず、ドライバーのioctlからサービスを割り当てる処理が必要だった。
通常はWi-Fiを優先し、LTEは大きいmetricで待機させた。 サービス停止時には、自分が追加したIP、経路、DNSだけを取り除く。 Wi-Fi側の設定が残ることと、携帯サービスを再起動して接続し直せることを確認した。
実通信の検査では、Wi-Fiを有効にしたまま、DNSのUDPソケットとHTTPSのTCPソケットの両方をSO_BINDTODEVICE=usb0で携帯側へ固定した。
送信元アドレスも携帯側を指定し、CAとホスト名の証明書検証を有効にしたままHTTPSの200応答を確認した。
「Wi-Fiがつながっている端末でWebが開けた」だけでは、LTE通信の証明にならない。 この検査で、携帯回線だけを通るDNSとHTTPSが成立したことを確認できた。
ステータスバーと物理ボタンを備える
アプリと共通表示を分ける
最初はSpotifyのGUIが画面全体を描いていた。 その後、アプリが終了しても時刻、通信状態、電池残量が残るように、共通の描画サービスを作った。
論理画面600×1024のうち、上44ピクセルをステータスバー、下600×980をアプリ領域に分けた。 フレームバッファーは共通サービスだけが描き、アプリは画像を渡す。 タッチ座標もアプリ領域へ変換する。
Wi-FiとLTEの表示は、通常の送信に選ばれる経路に合わせて一方だけを出すようにした。 接続そのものを排他的にしたのではなく、Wi-Fi優先とLTE待機の設定は維持している。 アプリが閉じた画面と起動時の案内も同じ描画処理へ揃えた。
ホームにはSpotifyのタイルを一つ置いた。 公式のブランド素材から取得したアイコンを使い、タップでSpotifyへ戻れるようにした。 再生、前後曲、シャッフル、リピートも、フォントの記号ではなく図形アイコンで描いた。 汎用のアプリ切り替え環境を完成させたのではなく、今あるSpotifyを開き直せるホームである。
物理ボタンを音楽操作へ割り当てる
前面の物理ボタンは次のように割り当てた。
| ボタン | 短押しの機能 |
|---|---|
| A1 | 前の曲 |
| A2 | 再生と一時停止の切り替え |
| A3 | 次の曲 |
| 電源 | ホームへ戻る |
入力装置はevent番号を固定せず、装置名で探索する。 キーリピートや短すぎる再操作、同時押しを抑制し、処理中の追加操作は捨てるようにした。 実機ではホーム復帰、再生停止、スキップが動くことを確認した。 前曲と次曲それぞれの全条件や、長押しまで確認したわけではない。
電源ボタンの短押しはGUI内のホーム復帰であり、一般的なスマートフォンのスリープ機能を作ったわけではない。
AUX用途に合わせて音量方針を変更する
初期の実装では安全側に倒し、イヤホン接続時の音量を20%以下に制限した。 スピーカーは100%まで操作でき、イヤホン挿入時に音量を下げる版も実機で確認した。
その後、3.5 mm端子をAUX出力にも使うため、この制限を解除した。 現在の実装はスピーカーと3.5 mm端子の双方で0〜100%を許可する。 ただし、抜去時の一時停止、出力切り替えと音量変更の排他制御、監視異常時の出力停止は残した。
AUX用途への対応は音量制限の変更であり、固定レベルのライン出力を新設したわけではない。 実際のAUX機器へ接続した発音試験は未実施で、イヤホンを使うときは大きな音量が保存されている可能性にも注意が必要になる。
起動時にSpotifyを開き、前回の音量で再生する
最終的には、OS起動後にSpotifyの画面を直接開き、保存した曲を再生するようにした。 ホーム画面は電源ボタンやアプリ終了後の戻り先として残している。
BusyBox initのinittabからGUIサービスを起動し、音声監視と通信の準備を待つ専用の子プロセスが再生を要求する。
同じ起動で何度も再生しないよう、boot IDを使って一度だけ要求を記録する。
表示プロセスの再起動だけで、利用者が止めた曲を勝手に再開しないためでもある。
最初の再起動試験では自動再生まで進んだが、音量が初期値20%へ戻った。 そこで、音量変更の成功後に設定を原子的に保存し、次回は保存音量を復元してから再生するようにした。 音量の復元に失敗したら自動再生しない。 保存値がない場合だけ20%を初期値として使う。
後続の再起動では、保存値と実音量がともに100%で一致し、起動後のSpotifyがplayingになることを複数回観測した。
その後はタッチ入力の更新と一時停止への遷移を記録した。
観測用スクリプトは別の完了マーカーを待って期限切れになったが、再生状態そのものは確認できており、自動再生の失敗とは扱っていない。
この再起動ではUSB ADB、Wi-Fi、LTEの経路と音声監視の復帰も確認した。 ただし、最後の再起動後に物理的な発音を改めて聴取したわけではない。 保存するのも最後の曲URIと音量が中心で、キュー全体や再生位置を厳密に復元する実装ではない。
できたことと、まだ確認していないこと
実機で確認できたこと
- 純正カーネルのままAlpineを直接起動し、BusyBoxをPID 1にできた。
- 画面、タッチ、満充電LED、5 GHz Wi-Fiを動かし、通常再起動後の復帰を確認した。
- Spotifyを本体で再生し、内蔵スピーカーとイヤホンの発音、日本語検索からの再生を確認した。
- MC7700のSIMロックを解除し、povo LTE限定のDNSと証明書検証付きHTTPSを確認した。
- 共通ステータスバー、タイルホーム、物理ボタン、起動時再生、音量保持を組み込んだ。
ストレージとセキュリティの制限
全体バックアップはあるが、全体像を実機へ戻す試験はしていない。 Androidが起動しない状態からの独立復旧も未確立である。
純正3.0.8+は古いままで、CCSecurityを無効にした構成を使う。
最初は固定のrootブリッジを使っていたが、後にadbdもroot常駐へ変更し、通常のadb shellでUID 0を使えるようにした。
信頼するUSB接続PCからの管理を前提としており、一般向けに安全な端末へ仕上げたという意味ではない。
音声と電源管理の制限
ロスレス表示は確認したが、受信コーデックとビット深度の独立監査はしていない。 AUX機器の実接続、最後の再起動後の発音聴取、0%保存からの再起動も未確認である。
長時間連続運転、サスペンド復帰、消費電力、電池残量の精度は評価が残っている。 満充電以外のLED状態も、実際の低残量や温度異常を起こして検証したわけではない。
通信とソフトウェア保守の制限
LTEは今回の場所とBand 1での実績であり、移動中の基地局切り替え、圏外復帰、実際にWi-Fiを切ったアプリの通信切り替えまでは検証していない。 Wi-FiのDFSチャンネルや長期切断からの復帰も未確認である。
採用したSoloistはビルドから90日で失効するため、更新が必要になる。 私有ライブラリーにはtime32互換の制約があり、Web APIには調査時点の開発用制限もある。 一度動いた構成をそのまま永久に使えるわけではない。
今回の改造で残ったもの
作業前のJT-B1は、Android 4.0.4が通常起動する端末だった。 現在は純正カーネルからAlpineを直接起動し、縦画面で日本語の曲検索ができ、スピーカーと3.5 mm端子へSpotifyを出力する構成になった。 Wi-Fiに加えてpovoのLTEでも通信でき、物理ボタンで音楽を操作できる。
この過程では、成功したように見える状態を何度も切り分け直した。 実効UID 0でもデバイスを読めず、画像を書いてもパネルに出ず、PCMが進んでも無音で、開発用認証が通ってもSIMロックは残った。 そのたびに、APIの戻り値、ドライバーの状態、実際の表示や音を別々に確かめる必要があった。
新しいカーネルを移植したわけでも、汎用モバイルOSを完成させたわけでもない。 純正の機種依存部分を残し、その上に必要なユーザー空間とGUIを積み直すことで、手元の一台を音楽プレーヤーとして使えるところまで進められた。
公開資料
以下は調査で利用した公開資料である。 リンク先の提供状況やソフトウェアの仕様は変わる可能性がある。

