USB 類別代碼記在裝置層與介面層兩處,只讀裝置層的印表機會回報 0
USB 規範用一個位元組的類別代碼描述裝置的用途,而同一個裝置身上有兩個地方可以記這個代碼:裝置描述符的 bDeviceClass,以及每個介面描述符的 bInterfaceClass。兩處各自獨立,值也可以不同。要按用途過濾裝置的程式若只讀其中一處,會在一整類硬體上靜默失效。
類別代碼本身是一組固定的數字
常見的幾個值:
| 代碼 | 用途 |
|---|---|
1 | 音訊 |
3 | 人機介面(HID):觸控面板、鍵盤、滑鼠、掃碼槍 |
7 | 印表機 |
8 | 大容量儲存 |
9 | 集線器 |
14 | 視訊 |
255 | 供應商自訂 |
這組數字由 USB-IF 定義、跨作業系統一致,所以它是可以拿來做判斷的穩定依據——比裝置名稱字串可靠得多,名稱字串由廠商自由填寫。
兩層的存在來自複合裝置
一個 USB 裝置可以同時提供多種功能:一台印表機附帶讀卡機、一支鍵盤內含集線器。規範用介面表達這種組合——裝置底下掛多個介面,每個介面各自宣告自己的類別。
於是裝置層的那個代碼面對一個問題:這台裝置整體算哪一類。規範給的答案是允許它填 0,語意是「看介面自己說」。這個選項對複合裝置是必要的,而廠商對單一功能的裝置也常常這樣填——熱感印表機的裝置層類別多半是 0,真正的 7 記在介面上。
這條路徑在 Android 上特別容易踩到,因為兩層各有各的取值方法而名字相似:
UsbDevice.getDeviceClass()讀的是裝置層,對熱感印表機回傳0UsbDevice.getInterface(i).getInterfaceClass()讀的是介面層,回傳7
0 在類別代碼裡有自己的語意(保留給「看介面」),與「查不到」不同,而程式若把它當成一個普通的類別值去比對 7,比對結果是不相符——沒有例外、沒有錯誤訊息,過濾條件安靜地把每一台印表機都排除掉。
判斷邏輯的單位是介面,不是裝置
由複合裝置推出兩個要求。
逐個介面檢查,而條件是「任一介面符合」。 一台裝置有三個介面而只有第二個是印表機時,只讀 getInterface(0) 得到的答案是錯的。
裝置層的值只在它非零時有參考價值。 它非零代表廠商明確宣告了整體用途,可以直接採用;它是 0 就必須往介面層走一趟。
255 讓白名單這條路走不通
供應商自訂類別是規範留給廠商的出口:裝置的功能不落在既有分類裡,或廠商選擇用自家驅動程式而不走通用類別驅動。而國產熱感印表機宣告成 255 的情形確實存在——這一條來自實作者的現場回報,不是本文核對過的裝置清單,所以它的效力止於存在性;要用到比例,得自己在手上的機型上量。
這推翻「只收類別 7」這個寫法的事實前提——那個前提是全稱的(每一台印表機都宣告 7),而一台反例就足以否定全稱命題。但「前提被否定」與「這個寫法該不該用」是兩層。 該不該用是工程權衡,要靠代價而不是靠邏輯:白名單誤擋的後果是那台印表機從候選裡整個消失、而使用者沒有任何操作把它找回來。這個代價與該機型的佔比無關——它對踩到的那一台是全損,所以即使佔比很低,白名單仍然是不划算的選擇。過濾策略因此要換一個方向——讓失敗往可補救的方向倒那一篇處理這個選擇。
判斷標準
要判定某個 USB 裝置的用途時,取值的順序由「哪一層的值可信」決定:裝置層非零就用它,裝置層是 0 就逐個讀介面層並取任一符合。
而判定結果為「不是印表機」與「查不到類別」要分開處置。判定為不是印表機是有效的排除,查不到類別代表資料不足——藍牙裝置沒有 USB 類別欄位,把取不到值當成不合格會誤殺一整類裝置。
驗證這件事的成本很低:adb shell dumpsys usb 會把每個裝置的兩層類別都列出來,接上機器跑一次就知道手上這台實際填了什麼,而不必從程式行為反推。盤點陌生機器的硬體那一篇有完整的查法。