白いタブレットの横に crDroid のロボットと、緑の丸に白文字の 16 を描いたイラスト

Xiaoxin Pad Pro 2021(TB-J716F)をAndroid 16 GSIで復活させた:BLU・文鎮化からの復旧・画面回転とカメラの90°ズレ

Kojiro Tanaka

ブートローダーの解除と、EDL(9008)でのパーティション書き込みは、失敗すると文鎮になる。保証は戻ってこないと思ったほうがいい。シリアル番号の書き換えも入る。同じ TB-J716F でもファームの版が違えば、ここに書いたオフセットはそのまま使えない。作業フォルダのバックアップが無い状態で、同じ手順を踏まないこと。

2021年に買った Xiaoxin Pad Pro 2021 が、引き出しの中で止まっていた。ZUI の最終版(Android 13 ベース)のまま、日本語は More Locale でつぎはぎ、そのうち焼こうと思って何年も置いた。

買ったときの感触は Xiaoxin Pad Pro 2021のレビュー に、5ヶ月使ったあとは 長期レビュー に書いてある。今回はその個体を、ブートローダーを開けて、一度殺して、crDroid 12.12 の GSI(Android 16 QPR2)で起こした記録。

作業は AI エージェントと一緒に進めた。ログと XML の読みは向こう、USB の抜き差しと「起きたか」の確認はこっち。画面とカメラの 90° は、別のエージェントがベンダーのセンサー定義まで降りて直した。

触った環境

項目値
機種Lenovo Xiaoxin Pad Pro 2021(TB-J716F、小新 Pad Pro 2021)
SoCSnapdragon 870(SM8250、kona)
パネル11.5インチ、2560×1600。自然な向きは横
放置していたファームZUI の最終版(Android 13 ベース)
入れた OScrDroid 12.12 GSI / Android 16 QPR2 + GApps(Doze-off ビルド 2026-09-22)
GSI のビルドtdgsi_arm64_ab、BP4A.251205.006
PCWindows。adb / fastboot 35.0.1、QPST / QFIL 2.7.496、Python 3.10

パネルは 11.5インチ。解像度が同じ 12.6インチの Xiaoxin Pad Pro / P12 Pro とは別機種で、前に書いたレビューの寸法がこっちの実機に合う。

COM ポートは作業時 COM5 だった。番号は PC による。作業フォルダは C:\Users\<user>\j716f_rescue\ のように切ってある。端末の元のシリアルはここには書かない。解錠のために書き換えたあとの値は、公開されているテンプレートの HAHAHAHA。

普通の unlock は拒否される

fastboot flashing unlock も、単体の oem unlock-go も通らない。返ってくるのは Flashing Unlock is not allowed。

ZUI タブレットのブートローダーは、fpinfo パーティションに入っているシリアルと、署名済みの sn.img が一致したときだけ解錠を許可する。作業メモには、公式の zui.com/iunlock からシリアルを申請すると、CDN にそのシリアル名の sn.img がある、という 2026年時点の報告が残っている。今回使ったのは公開テンプレートのほう。fpinfo の先頭 8バイト(ASCII のシリアル)を HAHAHAHA に書き換えて、その文字列で署名された無編集の sn.img を流す。sn.img 側を編集すると署名検証で Authenticate failed になる。合わせるのは端末側。

  1. EDL へ入る。fastboot oem reboot-edl。または電源を切って、音量+を押したまま USB を挿す。EDL はアイドルが数分続くと死ぬので、入った直後に firehose を載せる。
  2. Sahara が通ったことを確認する。
QSaharaServer.exe -s 13:prog_firehose_ddr.elf -p \\.\COM5

Sahara protocol completed が出るまでがこの段。

  1. QFIL の Tools → Partition Manager。
    • fpinfo を右クリック → Manage Partition Handles → Read Data。ダンプは %APPDATA%\Qualcomm\QFIL\COMPORT_5\ReadData_ufs_Lun0_0x2208_*.bin に出る。フォルダ名の 5 は COM 番号。
    • オフセット 0 の 8バイトを、元のシリアルから HAHAHAHA に書き換えて、Load Image で戻す。
    • frp も読んで、末尾バイトが 00 なら 01 にする。00 から 01 が OEM アンロック許可。すでに 01 なら触らない。
  2. Partition Manager を閉じる。電源を 20秒長押しして落としてから、電源+音量- でブートローダーへ。
  3. テンプレートの sn.img を焼いて解錠する。編集版は使わない。
fastboot flash unlock sn.img
fastboot oem unlock-go
fastboot set_active a

ロック中は set_active が not allowed in Lock State で落ちる。これが通るようになったら、解錠できている。

解錠したあとの再起動で落ちるときは、/metadata と /data の初期化を疑う。

やってはいけないことが二つある。fpinfo と frp を、通常のファーム焼きの XML に混ぜて書かない。QFIL 標準の rawprogram0.xml は filename が空で、実質スキップされる。*_WIPE_PARTITIONS.xml を使うとゼロ埋めになる。再ロックに加えてシリアルが消える。もう一つ、devinfo は critical 扱いで、ロック中は fastboot から書けない。書き換えるなら EDL。

userdata が消えて、どの OS も起きない

フラッシュの途中で fastboot ループになった。起動アニメで固まる、リカバリに落ちる、もある。fastboot -w はこう言う。

wipe task partition not found: userdata

root が取れる状態なら、これで足りる。

ls /dev/block/by-name/userdata

無い。GPT 上の userdata を、カーネルが捨てている。

最初は GPT が壊れた、で終わりにしかけた。半分は当たっている。Lenovo の QPST パッケージは、rawprogram*.xml が書く GPT をテンプレートのまま置いて、userdata のサイズをプレースホルダにしている。実サイズにするのは patch0.xml から patch5.xml。中身は次の一行。

Update last partition 18 'userdata' with actual size (NUM_DISK_SECTORS-6)

patch を流さないと、カーネルは userdata エントリを黙って破棄する。/dev/block/by-name/userdata が生まれないので、どの OS も起動できない。QFIL の GUI で Download すると rawprogram のあと patch まで連続で走る。途中で失敗したり、中断したりすると、patch だけ抜ける。今回の文鎮はこれだった。

復旧は、GPT を書き直して patch0〜5 を当て、QPST パッケージの純正ファームまで戻すこと。GSI はそのあと。

パッケージの欠損も同じ穴。super.img も metadata.img も userdata.img も入っていない。super の実体は rawprogram_unsparse0.xml の super_1.img から super_6.img。fh_loader は、filename のファイルが無いと黙ってスキップする。成功に見えて super が空、があり得る。書く前に、XML に出てくる filename が全部ディスク上にあるか見る。

9008 から戻す

GUI は XML の選び間違いと、ポートの掴み損ねが起きる。書き込みは fh_loader.exe のほうが確実だった。

fastboot oem reboot-edl
QSaharaServer.exe -s 13:prog_firehose_ddr.elf -p \\.\COM5
fh_loader.exe --port=\\.\COM5 --sendxml=XXX.xml --search_path=DIR --noprompt --memoryname=ufs

読み出しは癖がある。fh_loader は XML の read タグを無視する。--convertprogram2read と、program タグに filename="out.bin" を書いた形で出す。

fh_loader.exe --port=\\.\COM5 --convertprogram2read --sendxml=read.xml --search_path=DIR --noprompt --memoryname=ufs

ログは、実行したディレクトリの port_trace.txt。失敗したらここを見る。

リセット用には、次の XML を sendxml する。

<data><power value="reset" /></data>

これは OS を通らないハードリセットで、FBE の /data が汚れることがある。戻したあとの確認は adb reboot のほうがきれい。

EDL は数分放置すると、Sahara は通るのに、その後の firehose が全部無反応になる。操作の直前に入り直す。死んだら電源 20秒で強制オフ、fastboot、fastboot oem reboot-edl。

この端末の abl は、解錠後でも fastboot -w が erase だけで、フォーマットしない。初回起動の OS がフォーマットする。ロック中は set_active / flash / erase が全部 not allowed in Lock State。

fastbootd は、GSI を焼いた直後の初回だけ入り損ねることがある。落ち着いたあとは adb reboot fastboot で入る。

確認に使ったコマンドはこれ。

fastboot getvar all
fastboot oem device-info
adb shell getprop sys.boot_completed
adb shell su -c 'ls /dev/block/by-name/'
adb shell su -c 'cat /proc/partitions'

純正 boot じゃないと GSI は死ぬ

純正ファームに戻したあと、system に GSI を焼いた。手元に置いたイメージは 3つ。

いま動いている vendor は純正のまま。

ここで嵌ったのが boot。boot-debug.img だと GSI が起動しない。要るのは純正 boot。その純正 boot に Magisk v30.7 を当てたものを boot_a に焼いた。boot_b は純正のまま残してある。

Play ストアのログインがループする

サインインの「情報を確認しています」から、ログインボタンに戻る。com.google.android.gms.ui が落ちている。

SecurityException: getRoleHoldersAsUser ... MANAGE_ROLE_HOLDERS

GMS 26.40 が、MANAGE_ROLE_HOLDERS をマニフェストで要求していないのに RoleManager を呼ぶ。crDroid GSI のプラットフォーム権限との組み合わせで表に出た。

対処は、GMS を 25.15 に戻してログインし、そのあと 26.40 に上げる。ログイン済みなら 26.40 は安定して動く。落ちるのはアカウント追加のフロー。

設定の UI からはダウングレードできない。pm path com.google.android.gms が返す実体ディレクトリを root で消して再起動すると、25.15 に落ちる。パスの形はこう。XXXX と YYYY は pm path の実値で、固定文字列ではない。

rm -rf '/data/app/~~XXXX==/com.google.android.gms-YYYY=='

26 で作った GMS の内部 DB(RMQ テーブルなど)を 25.15 が読むと、no such table で gms.persistent が FATAL ループする。サインインは [no response] で失敗する。そのときは DB を作り直す。

pm clear com.google.android.gms
pm clear com.google.android.gsf

MANAGE_ROLE_HOLDERS などを足す Magisk モジュール gmsrolefix(privapp-permissions-gms-role-fix.xml)も入れて、いまも動いている。ログインそのものは、バージョンを往復させたほうが確実だった。26.40 に戻したあと、ログイン済みの状態で落ちないことは確認した。アカウントを足し直すときに同じクラッシュが出たら、同じ往復をやる。

GMS の版を見るときはこれ。

adb shell su -c 'dumpsys package com.google.android.gms | grep -E "versionCode|pkgFlags"'

画面もカメラも 90° ずれる

GSI が起きたあと、自動回転が実際の向きより 90° 時計回りにずれる。カメラのプレビューと撮影画像も 90° 回る。

Lenovo の純正ベンダーは「縦が自然な向き」の値を持っていて、ZUI のフレームワークがそれを吸収していた、という読み。GSI は 2560×1600 のパネルを、そのまま横の ROTATION_0 として扱う。ベンダー由来の向きが全部 90° ずれる。加速度とジャイロの軸が自動回転に、カメラのセンサー取り付け角がプレビューに出る。GSI 側の services.jar には、このオフセットを足すプロパティが無い。直す場所はベンダー。

WindowManager の実測は mPortraitRotation=ROTATION_270、mUpsideDownRotation=ROTATION_90。普通は 90 と 180。

ro.surface_flinger.primary_display_orientation=ORIENTATION_270 を Magisk の system.prop に書くと、画面のマッピングは正常になる。タッチ座標が追従しない。これは戻した。そのあいだは自動回転を切って、横固定で使っていた。

settings put system user_rotation 0

センサーの軸

Qualcomm SEE のレジストリで、bmi26x(実チップは bmi220)の軸が縦基準だった。ファイルは /vendor/etc/sensors/config/kona_hdk_bmi26x_0.json の .orient。

x = -y
y = +x
z = +z

WindowManager は Qualcomm の Device Orientation(type 27)で回転を決める。このセンサーは上の加速度から計算されるので、軸がそのまま回転判定に出る。

恒等に変えた。x=+x、y=+y、z=+z。レジストリは ver が大きいほうが勝つので、ver を 3 に上げた。

書く場所は二つ。

最初に x=-x、y=-y を試したら ROTATION_180 になった。レジストリの "x": "-y" は「出力 x = −入力 y」ではなく、逆写像として扱われているように見える。直感と逆に回る。実測で見る。

地磁気(ak0991x、+x,+y,+z)は触っていない。

カメラの bin

回転を直すと、画面の天地は合う。カメラの映像だけ 90° 回ったまま。Aperture で確認した。

dumpsys media.camera の android.sensor.orientation が縦基準だった。

カメラ修正前修正後
背面(2つ)900
前面(2つ)2700

CamX のセンサーモジュール、/vendor/lib64/camera/com.qti.sensormodule.*.bin の positionRoll / positionPitch / positionYaw は全部 360。360 は「カーネル dtsi の sensor-position-roll を使え」という意味で、dtsi 側が縦基準の 90 と 270 だった。

センサー軸を直す前後の比較から、純正の「縦の自然な向き」は、横の自然な向きを 90° 時計回りに回した姿勢にあたる。横(ROTATION_0)は純正座標系では ROTATION_90。

横基準ではどちらも 0°。

.bin は Chromatix Parameter Parser V2.0.0。フィールド名は先頭のシンボル表にあって、データ部は構造体のダンプ。ofilm_ov13b10 だとこの並び。

0x183b0  moduleName "ofilm" / sensorName "ov13b10" / actuator / eeprom / flash "pmic" / chromatix 名
0x18450  CSIInfo laneAssign = 0x3210
0x18460  double 360.0   positionRoll(SENSOR_ORIENTATION)
0x18468  double 360.0   positionPitch
0x18470  double 360.0   positionYaw

00 00 00 00 00 80 76 40(リトルエンディアンの double 360.0)が 3つ連続する場所は、各ファイルに 1つだけ。先頭の Roll を 0.0 にした。Roll が 360 以外なら、カーネル値より優先される。カメラを特定せず、全モジュールに同じ修正を入れた。

ファイルRoll のオフセット
com.qti.sensormodule.kingcome_gc5035.bin0x290b8
com.qti.sensormodule.kingcome_s5k4h7irkc.bin0xb7b8
com.qti.sensormodule.lce_s5k4h7rgb.bin0xb2c0
com.qti.sensormodule.ofilm_ov13b10.bin0x18460
com.qti.sensormodule.ofilm_s5k4h7irof.bin0xb2d0
com.qti.sensormodule.qtech_ov13b10.bin0x18470

パッチは 360.0 の三つ組を探して、先頭だけ指定角度で上書きする。三つ組がちょうど 1つでないファイルはスキップする。

import sys, os, glob, struct

# Rewrite CamX module positionRoll (first of the roll/pitch/yaw 360.0 triplet) to a fixed angle.
src_dir, out_dir, angle = sys.argv[1], sys.argv[2], float(sys.argv[3])
triplet = struct.pack("<d", 360.0) * 3
os.makedirs(out_dir, exist_ok=True)
for path in sorted(glob.glob(os.path.join(src_dir, "*.bin"))):
    d = bytearray(open(path, "rb").read())
    hits = []
    i = d.find(triplet)
    while i != -1:
        hits.append(i)
        i = d.find(triplet, i + 1)
    print(os.path.basename(path), [hex(h) for h in hits])
    if len(hits) != 1:
        print("  skip: expected exactly one triplet")
        continue
    struct.pack_into("<d", d, hits[0], angle)
    open(os.path.join(out_dir, os.path.basename(path)), "wb").write(d)
python -I patch_roll.py 元binのディレクトリ モジュール/system/vendor/lib64/camera 0

/vendor/lib64/camera は adb pull だと Permission denied になる。su -c で /data/local/tmp にコピーしてから pull する。

確認はこれ。全カメラが Orientation: 0 になり、プレビューも撮影画像も向きが合った。

adb shell "dumpsys media.camera | grep -E 'Facing|Orientation:'"

Magisk モジュールはこう置いた。persist の bmi26x_0_platform.orient はモジュールの外。

/data/adb/modules/sensor_rotfix/
├── module.prop
└── system/vendor/
    ├── etc/sensors/config/kona_hdk_bmi26x_0.json
    └── lib64/camera/com.qti.sensormodule.*.bin

module.prop の id は sensor_rotfix。json のコンテキストは u:object_r:vendor_configs_file:s0。bin は u:object_r:vendor_file:s0、モード 644。

戻すとき。カメラだけなら、モジュールから system/vendor/lib64/camera を消して再起動する。センサーも戻すなら、Magisk から sensor_rotfix を消す。persist のほうは、バックアップ /sdcard/rotfix_backup/bmi26x_0_platform.orient の ver を 4 以上に上げて書き戻して再起動する。ver を上げないと、いまの ver 3 が残る可能性がある。この戻しは未検証。

バックアップは端末の /sdcard/rotfix_backup/(bmi26x_0_platform.orient と kona_hdk_bmi26x_0.json)と、作業 PC 側の一時ディレクトリ。

いまの端末

TB-J716F の設定画面。モデルは Lenovo TB-J716F、Android バージョンは 16、crDroid バージョンは 12.12

設定のデバイス情報は、モデル Lenovo TB-J716F、Android 16、crDroid 12.12。IMEI はタップしないと出ない画面のまま撮ってある。

保証も、戻し方も、まだ雑なところがある

シリアルは HAHAHAHA。保証や正規修理に出すなら戻す。戻しは未実施。解錠前のダンプをそのまま書き戻したときは、古いフラグが混ざって起動に支障が出た。1回ある。安全側は、EDL でいまの fpinfo を読んで、先頭 8バイトだけ元のシリアルに戻す。フラグは現行のまま。戻したあとは adb reboot。EDL のハードリセットは使わない。元ダンプは C:\Users\<user>\j716f_rescue\fpinfo_ORIGINAL.bin。

*_WIPE_PARTITIONS.xml はゼロ埋め。再ロックとシリアル消失。開かない。

sn.img の編集は署名で死ぬ。fpinfo と frp を通常フラッシュで書かない。

センサー設定の戻し(ver を 4 以上にして persist へ書き戻す)は未検証。

GMS は 26.40 でログイン済みなら安定している。アカウントを足す操作では、同じ MANAGE_ROLE_HOLDERS が出うる。26 と 25.15 を往復すると DB のスキーマが合わなくなるので、そのときは pm clear。

回転のもう一つの根本は、純正 vendor をより新しい世代に上げること、という話がコミュニティにはある。今回は上げていない。上げるなら、カメラの向きも縦基準が残る問題として見る。メモに残っている恒久案は、LSPosed の後継(Vector Framework、Android 16 対応)で SENSOR_ORIENTATION をフックする、というもの。実機ではやっていない。背面 90 / 前面 270 がランドスケープのパネルとずれている、という読みは、今回直した CamX の値と一致している。

この記録は TB-J716F を 1台、純正ファームの上に焼いたもの。カメラ bin のオフセットは、360.0 の三つ組が 1つだけ、という条件付き。スクリプトが skip と出したら、そのファイルは別の構造。

同じ端末を EDL の文鎮から戻すには、作業フォルダ一式が要る。クラウドか別ディスクにコピーしておく価値がある。目印だけ置いておく。

Doze-off に出すと同じ機種が楽になる、とメモにあるのは次の 2つ。報告はまだ出していない。

エージェントと一緒に切った

放置が長すぎて、手順を頭の中だけに持っていなかった。アンロックの系統、EDL の XML、GSI が死ぬ理由、GMS のスタック、回転の軸。どれも実機のログを見ながら、エージェントと一緒に切り分けた。

役に立ったのは「次のコマンド」より、失敗ログの読みだった。fastboot -w の userdata not found を、patch フェーズの抜けまで戻してくれたのが大きい。QFIL が成功したように見えて patch だけ欠ける、という読みは、自分だけではパッケージの XML を全部開いていない。

カメラの bin は、フィールド名がシンボル表にしか無くて、データ部が double の並び、というところまで別のエージェントが追った。オフセットをここに書けているのはそのメモがあるから。角度の式(背面は 90−90、前面は 270+90)も、軸を直す前後の比較から出している。プレビューが回ったかは、手元の画面で見た。

エージェントが先に出した画面回転の直し(primary_display_orientation)は、タッチが付いてこないので捨てた。

シェア

関連記事

定番の記事

← 記事一覧へ