ByteScope

Android ADB Toolbox

十一件小的 adb 工作變成按鈕——無線除錯、截圖、文字輸入、deep link、螢幕尺寸、proxy、系統屬性與當機紀錄。

每一條指令都走 USB 線執行,輸出留在這個分頁裡。沒有任何上傳,截圖與匯出都是瀏覽器直接寫進你自己的磁碟。

正在檢查瀏覽器支援…

無線除錯(透過 Wi-Fi 的 adb)

把手機的 ADB daemon 切到 TCP 連接埠 5555,並產生給桌機終端機用的連線指令。兩台機器要在同一個網路,而這個切換在手機重開機後就沒了。

adb tcpip 5555

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

截圖

用手機自己的 screencap 拍一張 PNG,以原始解析度透過線傳回來。有 DRM 保護的內容會是全黑,因為合成器不會把那些畫面交給任何人。

screencap -p

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

傳送文字與按鍵

往手機上目前有焦點的地方打字,用的是平台給 adb 的同一個 input text。長密碼、URL、不想用拇指輸入的測試資料都很好用。

input text 'hello world' input keyevent 3

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

假造螢幕尺寸與密度(wm size)

讓手機假裝自己是另一塊螢幕,這是在真機上看不同斷點版面最便宜的方法。斷點反應的是 dp——像素乘 160 再除以 dpi——所以每個預設檔都同時設兩個值。

wm size 1080x2340 wm density 420 wm size reset wm density reset

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

開發者開關

把手機開發者選項裡的開關搬出來,不用點過六層選單:動畫速度、顯示觸控、指標位置、不保留 activity、強制從右到左的版面。

settings put global window_animation_scale 0.0 settings put system show_touches 1

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

裝置 proxy(mitmproxy、Charles、Fiddler)

把手機的 HTTP 堆疊指向你機器上的 proxy,就是每份抓包教學開頭那段接線的一行版。清除只要按一下,而它是這一節的第一顆按鈕,這是刻意的。

settings put global http_proxy 10.0.0.5:8080 settings put global http_proxy :0

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

系統屬性(getprop)

整張屬性表——通常超過一千筆——可以搜尋,而回答「這是什麼裝置」的那十五筆釘在最上面。你在這裡打的字不會到手機上:搜尋是在這個分頁裡做的。

getprop

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

可除錯 app 的資料庫

列出 debug 版 app 私有資料目錄裡的 SQLite 檔並取出一個,不需要 root。這是 run-as 真正有用的那一半:shell 讀不到 app 的資料,但 app 自己的 uid 讀得到,而 run-as 會把那個 uid 借你用一條指令。

run-as com.example.app ls -l databases run-as com.example.app cat 'databases/app.db'

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

最近的當機與 ANR

讀 DropBoxManager——平台自己那個「出了什麼事」的環形緩衝:app 當機、ANR、原生當機、Log.wtf 與 kernel 的 tombstone,都帶著時間。

dumpsys dropbox dumpsys dropbox --print data_app_crash

接上手機才能用這一節。上面那條指令在任何 shell 裡都能用。

螢幕錄影

錄影跟鏡射放在同一頁而不是這裡,因為兩者都是關於螢幕,而且 screenrecord 有一份實作比兩份安全——那個程序必須在每一條路徑上都被停掉,否則它會在沒人看的情況下一直編碼。

screenrecord --time-limit 180 --bit-rate 20000000 /sdcard/out.mp4

到螢幕鏡射頁面錄影 →

瀏覽器用 ADB 做不到的事

這不是一張等著被實作的功能清單。這些是平台或瀏覽器沙箱讓它不可能的事,在你開始找繞法之前,值得先知道原因。

  • adb forward / adb reverse

    連接埠轉發需要一個 TCP listener,而網頁不可能是。任何瀏覽器的分頁都沒有伺服器 socket,這是設計如此——也就是說你機器上的 localhost:8080 沒有東西可以指。這一項留給桌機的 adb。

  • adb backup / adb restore

    adb backup 在 Android 12 被標為棄用,而更早之前多數 app 就用 allowBackup=false 退出了,所以它在現代手機上產出的是一個幾乎空的封存檔。真正要的檔案,用檔案瀏覽器取出。

  • fastboot

    fastboot 是另一個 USB 介面上的另一套協定,說話的是 bootloader 而不是 Android。它可以用 WebUSB 實作,但 ADB 那條路徑的程式碼一點都用不上,所以它該是獨立的工具,而不是這裡的一個勾選框。

  • input text (non-ASCII)

    input text 只能產生虛擬鍵盤按鍵對照表裡有的字元,也就是可列印的 ASCII。這是平台指令本身的限制:桌機的 adb 一樣打不出中文。

  • adb root / su

    adb root 只在 eng 或 userdebug 版本上有用,而 su 只存在於 root 過的手機。這個頁面上的一切都是照著兩者都沒有來設計的,這也是其中某些功能會禮貌地拒絕的原因。

這個頁面上其他所有東西,都是在一支市售手機上、只把 USB 除錯打開、透過普通的 ADB shell 執行的,沒有任何要安裝的東西。

關於這個工具、FAQ 與相關工具

關於這個工具

把 Android 手機用 USB 接上 Chromium 系瀏覽器,這個頁面就把你每次都要重查的十一種 adb shell 指令做成按鈕,並把輸出解析成看得懂的樣子:把 daemon 切到 Wi-Fi、拍一張狀態列裡沒有你私事的截圖、打字與按實體按鍵、丟一個 deep link 並讀啟動結果、讓螢幕假裝成別的尺寸、切換開發者開關、把手機指向除錯用 proxy、搜尋一千兩百多筆系統屬性、取出 debug 版 app 自己的資料庫、讀最近的當機與 ANR。每一節都直接顯示它實際執行的指令,你可以帶走、在終端機裡用。手機上不安裝任何東西,也沒有任何上傳——這是一個沒有伺服器的靜態頁面。

無線除錯,以及瀏覽器唯一做不到的那件事

這個頁面可以把手機的 ADB daemon 切到 TCP 連接埠 5555,也可以讀出手機自己的 IP 位址,因為這兩件事都走在它已經有的 USB 連線上。它做不到的是接著連上那個連接埠。網頁沒有原始 TCP socket:fetch 說的是 HTTP,而 ADB 的交握不是 HTTP;WebSocket 需要對面是 WebSocket 伺服器,ADB daemon 不是;WebTransport 需要的 QUIC 端點也不是。沒有任何旗標、權限對話框或 origin trial 會改變這件事,這是沙箱照著設計在運作。所以這個功能誠實的形狀,就是一個把開關打開、然後把 adb connect 192.168.1.23:5555 交給桌機終端機的精靈——同樣的理由,你在這裡永遠不會看到 adb forward

不會把通知一起拍進去的截圖

screencap -p 負責拍,位元組以原始解析度透過線傳回來,光是不必事後再想辦法把圖傳出手機,就已經比在手機上截圖省事。真正值得知道的是它旁邊那個功能:SystemUI 的 demo 模式,是平台自己用來把狀態列固定成固定時間、滿電且沒有充電圖示、滿格訊號、沒有通知圖示的機制,商店頁面的圖就是這樣做出來的。有兩件事很容易弄錯,這個頁面都處理了——sysui_demo_allowed 沒開啟時整個模式會被忽略,所以頁面會先探測那個設定,而不是把廣播丟進黑洞;而被留在 demo 模式裡的手機會一直顯示假的時間,直到有東西關掉它,所以離開的按鈕一直看得見。

往手機打字,以及為什麼中文不可能

input text 是這個頁面上最細緻的指令,因為要送的字串是你的,而它要穿過兩層解釋器:一個是裝置的 shell,在那裡 a; reboot 會是兩條指令;另一個是 input 自己,它會在任何東西送到鍵盤之前把 %s 換成空白。所以文字是以一個單引號包起來的字送出,在單引號裡空白、&;$、反引號與管線都只是位元組。只有三個例外被明確處理:單引號用按鍵事件打出來而不是去跳脫它、換行變成 Enter、s 前面的 % 被切成獨立的一條指令,讓平台吃不到它。修不了的是字元集。input text 最後會走到只認得可列印 ASCII 的虛擬鍵盤按鍵對照表,所以中文、日文與帶重音的字母什麼都打不出來。這是 ADB 指令的限制,桌機的 adb 一樣打不出來。

Deep link,以及 -W 真正告訴你的事

am start -W -a android.intent.action.VIEW -d <url> 是確認你的 intent filter 到底有沒有接住某個 URL 最快的方法,而這個頁面會把回應好好讀完:Status: ok 會帶著贏的那個 activity 與啟動時間,unable to resolve Intent 本身就是一個答案而不是錯誤,而且通常正是你要測的那件事。有兩個細節值得寫出來。URL 正是那種會正當地含有 & 與 ? 的字串,而沒有被引號包住的 & 會把指令丟到背景並默默丟掉半個 query——這是手打的 deep link 測試「看起來沒用」最經典的原因。而當 app 本來就在前景時,手機會印出一個警告而完全不印時間;這個頁面會照實說,而不是給你一個讓人安心的 0 ms。

假造螢幕尺寸,以及回頭的路

wm size 1080x2340wm density 420 讓手機假裝成另一塊螢幕,這是在真機上看不同斷點版面最便宜的辦法。因為斷點反應的是與密度無關的像素,也就是像素乘 160 再除以 dpi,所以這裡每個預設檔都同時設兩個數字,並用它產生的 dp 寬度命名:320、360、411、480、800。接著是警告,而它是這個頁面上真正的那一個。覆寫會撐過重開機,並且改變*所有東西*的繪製尺寸,包括你要拿來取消它的設定 app;密度設錯會讓按鈕跑到螢幕邊界外,變成一支你沒辦法用它自己修好的手機。所以還原按鈕永遠就在預設檔旁邊,確認對話框在執行前就顯示 wm size resetwm density reset,而且不管這個覆寫是不是這個頁面設的,兩者都有效。

開發者開關,以及你一定會忘記的 proxy

動畫倍率、顯示觸控、指標位置、不保留 activity、強制從右到左的版面,全都只差一次 settings put,而這個頁面會在寫入*之前*先讀目前的值,所以它記下的還原指令就是原本真的在那裡的值。proxy 是同一套機制,卻是大得多的陷阱:settings put global http_proxy 10.0.0.5:8080 會把手機的 HTTP 堆疊指向你筆電上的 mitmproxy 或 Charles,而如果你設著它就走出這棟樓,手機在任何地方都上不了網——換一個 Wi-Fi 不行,行動網路也不行——而手機上完全看不出原因。這就是為什麼清除是那一節的第一顆按鈕而不是最後一顆,為什麼 settings put global http_proxy :0 會被當成解藥顯示出來,也為什麼清除按鈕對不是這個頁面設的 proxy 一樣有效。

系統屬性、app 的資料庫,以及當機紀錄

最後三節都是唯讀的,而一次除錯通常就結束在這裡。屬性瀏覽器搜尋整個 getprop(現代手機超過一千筆),把回答「這是什麼裝置」的十五筆釘在最上面,並提供把鍵排序過的 JSON 匯出,讓兩支手機的差異好比對。app 資料庫那一節用的是 run-as,它會把 app 自己的 uid 借給 shell 一條指令的時間,所以 debug 版的 SQLite 檔可以在沒有 root 的情況下列出並取出;release 版會被平台拒絕,而這個頁面會告訴你是哪一種情況,而不是印一個籠統的失敗。當機那一節讀的是 DropBoxManager,這才是「不 root 怎麼讀 ANR trace」真正的答案:現代版本上 shell 使用者讀不到 /data/anr,但同樣的 trace 在 drop box 裡,而 dumpsys 會交出來。

這些都不是在你自己機器上裝 platform-tools 的替代品——指令記得起來的話,終端機更快,而這個頁面很樂意把那些指令給你看。它擅長的是那支不是你的手機、那台不是你的電腦,以及那個沒有時間裝 SDK 的五分鐘。從這裡,Web ADB 的 hub 有裝置儀表板與家族其他成員,logcat 檢視器讓你看到你在動手時手機說了什麼,而螢幕那一頁負責鏡射或錄影。

常見問題

怎麼從瀏覽器用無線除錯?

按這裡的開關,手機的 ADB daemon 就會開始在 TCP 5555 上等待連線;頁面會讀出手機的 Wi-Fi 位址,給你 `adb connect <ip>:5555` 貼到你電腦的終端機。連線本身必須由桌機的 `adb` 完成,因為瀏覽器分頁沒有原始 TCP socket,也沒有任何旗標能改變這點。兩台機器要在同一個網路上,而這個切換在手機重開機後就沒了,所以下次插上線再按一次就好。要回到只用 USB 除錯,執行 `adb usb` 或把手機重開機。

為什麼 input text 打不出中文或日文?

因為 `input text` 做不到。這個指令最後會走到 Android 虛擬鍵盤的字元對照表,那裡只有可列印 ASCII 的按鍵,所以超出這個範圍的字元完全不會產生按鍵事件——有些版本會明確失敗,有些則是默默什麼都沒打。這是平台指令的限制,不是 USB、不是瀏覽器、也不是這個頁面的限制:桌機的 `adb shell input text` 一樣打不出來。實務上的答案是手機上的輸入法 app,或是剪貼簿。這個頁面會一開始就拒絕非 ASCII,並指出它送不出去的那個字元。

怎麼清掉我在手機上設的 ADB proxy?

`settings put global http_proxy :0`——proxy 那一節的清除按鈕做的就是這件事,不管那個 proxy 是不是這個頁面設的。`:0` 是關鍵:空字串在好幾種版本上會被原封不動存起來,讓手機還是指著一個沒用的地方,而 `:0` 才是平台自己的設定畫面會寫的值。拔線前先做這件事很值得,因為設著 proxy 離開這棟樓的手機在任何網路上都上不了網,也不會給任何提示。這個設定是全域的,所以行動網路上也一樣生效,不只 Wi-Fi。

Android 的 demo 模式是什麼,為什麼我的狀態列沒變?

demo 模式是有文件記載的 SystemUI 功能,會把狀態列固定成固定時間、100% 電量且沒有充電圖示、滿格訊號、沒有通知圖示——它存在的目的就是讓商店頁面的截圖不會順便把你的未讀訊息拍進去。如果送出後什麼都沒發生,最常見的原因是那道門:除非 `sysui_demo_allowed` 是 1,SystemUI 會忽略每一個 demo 廣播,而它預設不是 1。這個頁面會探測那個設定,並在記下還原方式之後提議打開它。另一個原因是製造商把接收器拿掉了,那再怎麼廣播都不會有用。

為什麼 run-as 說我的 app 不是 debuggable?

因為它是 release 版,而 `run-as` 只在那個 app 的 manifest 標記為可除錯時才會把它的 uid 借給你。Play 商店裡的每個 app 都是 release 版,所以這一節是用在你自己編出來的東西上,不是你安裝的東西上。從 shell 沒有繞過的方法:要讀另一個 app 的私有資料,需要同一個 app 的 debug 版,或是一支 root 過的手機。如果訊息說的是找不到那個套件,那就是名稱錯了,或者那個 app 裝在跟你正在看的不同的使用者或工作設定檔裡。