tande lab.

[Android] adb_usb.ini の正しい書き方

突然ですが、Android実機で開発してる人はだいたい端末毎のUSB Vendor idを書くのにadb_usb.iniいじってますよね。ただこのファイル、一行目にDO NOT EDITで書いてある。

# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.
# USE 'android update adb' TO GENERATE.

一通りググってみても、みんなここにVendor id書けと書いてあるんだが、このDO NOT EDITが気になって仕方が無い。

http://developer.android.com/intl/ja/guide/developing/device.html#setting-up
ここ見てもadb_usb.iniをいじれとは書いてない・・・

と言うことで調べてみた。

結論

話が長くなるので先に結論を書いてしまうと、Android SDKのadd-onとして追加するのが正しいやり方っぽい。

SDK add-onに新規フォルダを作成

SDKのインストール先の直下にadd-onsというフォルダがあるので、その中に適当な名前のフォルダを作成する。

例 $SDK/add-ons/my-add-on

作成したフォルダの中にmanifest.iniを作成

以下の様なテキストファイルを、manifest.iniというファイル名で上で作成したフォルダに作る。

name=Sony Tablet unofficial usb-vendor addon
vendor=tande
description=Adds USB support for Tablet S & P (Vendor id:0x054c)
api=11
revision=1
usb-vendor=0x054c

これはSony tabletの例。name、vendor、descriptionは適当に(ここに設定したものがSDK Managerのリストに表示される)。
apiにはインストール済みのAPI Levelを適当に設定します。usb-vendorに16進4桁でココにあるVendor idを記載する。

android update adbを呼ぶ

adb_usb.iniに書かれているように、android update adbを呼び出す。androidというコマンドはSDKのtools以下にあります。

たぶんこれは最初の一回だけでオーケー

以上。これでadb_usb.iniにvendor idが書き込まれます。SDKやGoogle USB Driverをアップデートしても自動的に書き込まれるのでをアップデートしたらデバイス繋がらなくなったーなんてことも無くなります。

android update adbは何をするのか

ここからはこの結論に至った流れを説明しましょ。

まずは、adb_usb.iniに

# USE 'android update adb' TO GENERATE.

と書いてあるので、素直に

$SDK/tools/android update adb

を実行してみると・・・

何も書き込まれずorz

android コマンドの実体は、windowsの場合android.batです。
中を見てみると、最後に

call %java_exe% %REMOTE_DEBUG%
-Dcom.android.sdkmanager.toolsdir="%tools_dir%"
-Dcom.android.sdkmanager.workdir=%work_dir% -classpath
"%jar_path%;%swt_path%\swt.jar" com.android.sdkmanager.Main %*

と書いてある。
結局sdkmanagerを呼び出しているだけの模様

SDK ManagerはAndroid SDKのアップデート時に表示されるあのUIのこと。コマンドラインからも触れるようになってるんだな。
さて、このソースはgitから取ってくる必要がありめんどくさいのでググってみると・・・

見つけた
Eclairのさらにmodっぽいurlだけどこの辺はそんなに変わってないと勝手に予想。
http://www.androidadb.com/source/pdn-slatedroid-read-only/eclair/sdk/sdkmanager/app/src/com/android/sdkmanager/Main.java.html

ここでandroid.batに渡される(実際にはSDKManager.Mainに渡される)引数のupdate adbが何をするか調べてみる。

エントリポイント

   /**
    * Runs the sdk manager app
    */
   private void run(String[] args) {
       createLogger();
       init();
       mSdkCommandLine.parseArgs(args);
       parseSdk();
       doAction();
   }

引数をパースしているSDKCommandLineはこれっぽい。
http://www.androidadb.com/source/pdn-slatedroid-read-only/eclair/sdk/sdkmanager/app/src/com/android/sdkmanager/SdkCommandLine.java.html

この中の38行目からパラメータ名定義が並んでるので

   public final static String VERB_LIST   = "list";
   public final static String VERB_CREATE = "create";
   public final static String VERB_MOVE   = "move";
   public final static String VERB_DELETE = "delete";
   public final static String VERB_UPDATE = "update";

   public static final String OBJECT_SDK          = "sdk";
   public static final String OBJECT_AVD          = "avd";
   public static final String OBJECT_AVDS         = "avds";
   public static final String OBJECT_TARGET       = "target";
   public static final String OBJECT_TARGETS      = "targets";
   public static final String OBJECT_PROJECT      = "project";
   public static final String OBJECT_TEST_PROJECT = "test-project";
   public static final String OBJECT_ADB          = "adb";

update adbはそれぞれVERB_UPDATEとOBJECT_ADBとして定義されているのでSdkCommandLineの中を検索すると・・・ない

改めてsdkmanager/Main.java側で探してみると発見

   /**
    * Actually do an action...
    */
   private void doAction() {
       String verb = mSdkCommandLine.getVerb();
       String directObject = mSdkCommandLine.getDirectObject();

       //...

       } else if (SdkCommandLine.VERB_UPDATE.equals(verb) &&
               SdkCommandLine.OBJECT_ADB.equals(directObject)) {
           updateAdb();

       } else {

updateAdb()の中身は

   /**
    * Updates adb with the USB devices declared in the SDK add-ons.
    */
   private void updateAdb() {
       try {
           mSdkManager.updateAdb();

           mSdkLog.printf(
                   "adb has been updated. You must restart adb with the following commands\n" +
                   "\tadb kill-server\n" +
                   "\tadb start-server\n");
       } catch (AndroidLocationException e) {
           errorAndExit(e.getMessage());
       } catch (IOException e) {
           errorAndExit(e.getMessage());
       }
   }

mSdkManager.updateAdb()が処理の実体の模様。このmSdkManagerは次のところで生成されてる。

   /**
    * Does the basic SDK parsing required for all actions
    */
   private void parseSdk() {
       mSdkManager = SdkManager.createManager(mOsSdkFolder, mSdkLog);

       if (mSdkManager == null) {
           errorAndExit("Unable to parse SDK content.");
       }
   }

これはcom.android.sdklib.SdkManagerのインスタンスぽいので同じようにググってみる。
http://www.androidadb.com/source/pdn-slatedroid-read-only/eclair/sdk/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkManager.java.html

updateAdb()は

   /**
    * Updates adb with the USB devices declared in the SDK add-ons.
    * @throws AndroidLocationException
    * @throws IOException
    */
   public void updateAdb() throws AndroidLocationException, IOException {
       FileWriter writer = null;
       try {
           // get the android prefs location to know where to write the file.
           File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE);
           writer = new FileWriter(adbIni);

           // first, put all the vendor id in an HashSet to remove duplicate.
           HashSet<Integer> set = new HashSet<Integer>();
           IAndroidTarget[] targets = getTargets();
           for (IAndroidTarget target : targets) {
               if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) {
                   set.add(target.getUsbVendorId());
               }
           }

           // write file header.
           writer.write(ADB_INI_HEADER);

           // now write the Id in a text file, one per line.
           for (Integer i : set) {
               writer.write(String.format("0x%04x\n", i));
           }
       } finally {
           if (writer != null) {
               writer.close();
           }
       }
   }

SDK Addons の文字が出てきた。
気になるADB_INI_FILEの定義を見ると、

   /** Preference file containing the usb ids for adb */
   private final static String ADB_INI_FILE = "adb_usb.ini";
      //0--------90--------90--------90--------90--------90--------90--------90--------9
   private final static String ADB_INI_HEADER =
       "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" +
       "# USE 'android update adb' TO GENERATE.\n" +
       "# 1 USB VENDOR ID PER LINE.\n";

きました。ここで生成してるのは間違いない。

getTargets()でとってきたtargetなるものからgetUsbVendorId()を呼んで、IAndroidTarget.NO_USB_IDでなければHashSetに追加し、最後にsetの中身をwriter.write(String.format("0x%04x\n", i))している。つまり、何とかしてgetTargets()で列挙されるようにすれば、android update adbでadb_usb.iniに正しくVendor-idが書き込まれるはず。

getTargets()の定義は

   /**
    * Returns the targets that are available in the SDK.
    * <p/>
    * The array can be empty but not null.
    */
   public IAndroidTarget[] getTargets() {
       return mTargets;
   }

mTargetsへの書き込みは

   /**
    * Sets the targets that are available in the SDK.
    * <p/>
    * The array can be empty but not null.
    */
   private void setTargets(IAndroidTarget[] targets) {
       assert targets != null;
       mTargets = targets;
   }

setTargetsの呼び出しは、

   /**
    * Creates an {@link SdkManager} for a given sdk location.
    * @param sdkLocation the location of the SDK.
    * @param log the ISdkLog object receiving warning/error from the parsing.
    * @return the created {@link SdkManager} or null if the location is not valid.
    */
   public static SdkManager createManager(String sdkLocation, ISdkLog log)
       try {
           SdkManager manager = new SdkManager(sdkLocation);
           ArrayList<IAndroidTarget> list = new ArrayList<IAndroidTarget>();
           loadPlatforms(sdkLocation, list, log);
           loadAddOns(sdkLocation, list, log);

           // sort the targets/add-ons
           Collections.sort(list);

           manager.setTargets(list.toArray(new IAndroidTarget[list.size()]));

           // load the samples, after the targets have been set.
           manager.loadSamples(log);

           return manager;
       } catch (IllegalArgumentException e) {
           if (log != null) {
               log.error(e, "Error parsing the sdk.");
           }
       }

       return null;
   }

あとreloadSdk()でも呼ばれてるけど、まぁcreateManagerかな。

setTargets()に渡されているIAndroidTargetのリストはloadPlatforms()とloadAddOns()で追加されているっぽい。見るべきはloadAddOns()と予想して先に進む。

   /**
    * Loads the Add-on from the SDK.
    * @param location Location of the SDK
    * @param list the list to fill with the add-ons.
    * @param log the ISdkLog object receiving warning/error from the parsing.
    */
   private static void loadAddOns(String location, ArrayList<IAndroidTarget> list, ISdkLog log) {
       File addonFolder = new File(location, SdkConstants.FD_ADDONS);
       if (addonFolder.isDirectory()) {
           File[] addons  = addonFolder.listFiles();

           for (File addon : addons) {
               // Add-ons have to be folders. Ignore files and no need to warn about them.
               if (addon.isDirectory()) {
                   AddOnTarget target = loadAddon(addon, list, log);
                   if (target != null) {
                       list.add(target);
                   }
               }
           }

           return;
       }

       String message = null;
       if (addonFolder.exists() == false) {
           message = "%s is missing.";
       } else {
           message = "%s is not a folder.";
       }

       throw new IllegalArgumentException(String.format(message,
               addonFolder.getAbsolutePath()));
   }

add-onsフォルダの中にあるフォルダを順次loadAddon()で呼び出してるので、さっさとloadAddon()の中へ。
(このコードだとSDK Root直下をなめているような気がするけど、きっとEclairだから)

loadAddon()の中はちょっと長いので省略しながら

   /**
    * Loads a specific Add-on at a given location.
    * @param addon the location of the addon.
    * @param targetList The list of Android target that were already
loaded from the SDK.
    * @param log the ISdkLog object receiving warning/error from the parsing.
    */
   private static AddOnTarget loadAddon(File addon, ArrayList<IAndroidTarget> targetList,
           ISdkLog log) {
       File addOnManifest = new File(addon, SdkConstants.FN_MANIFEST_INI);

       if (addOnManifest.isFile()) {
           Map<String, String> propertyMap = parsePropertyFile(addOnManifest, log);

add-onsフォルダのSdkConstants.FN_MANIFEST_INIというファイルをopenして、parsePropertyFile()で
ini形式のフォーマットをMapに詰める(中身見てないけど)。

           if (propertyMap != null) {
               // look for some specific values in the map.
               // we require name, vendor, and api
               String name = propertyMap.get(ADDON_NAME);
               if (name == null) {
                   displayAddonManifestError(log, addon.getName(), ADDON_NAME);
                   return null;
               }

               String vendor = propertyMap.get(ADDON_VENDOR);
               if (vendor == null) {
                   displayAddonManifestError(log, addon.getName(), ADDON_VENDOR);
                   return null;
               }

               String api = propertyMap.get(ADDON_API);
               PlatformTarget baseTarget = null;
               if (api == null) {
                   displayAddonManifestError(log, addon.getName(), ADDON_API);
                   return null;
               } else {
                   // Look for a platform that has a matching api level or codename.
                   for (IAndroidTarget target : targetList) {
                       if (target.isPlatform() && target.getVersion().equals(api)) {
                           baseTarget = (PlatformTarget)target;
                           break;
                       }
                   }

                   if (baseTarget == null) {
                       // Ignore this add-on.
                       if (log != null) {
                           log.error(null,
                                   "Ignoring add-on '%1$s': Unable to find base platform with API level '%2$s'",
                                   addon.getName(), api);
                       }
                       return null;
                   }
               }

ini中のプロパティ名ADDON_NAME、ADDON_VENDOR、ADDON_APIがそれぞれ記載されてないとエラーになりますと書いてます。ADDON_NAMEとADDON_VENDORは文字列、ADDON_APIはSDKに含まれるAPIレベルである必要がありそう。

その後しばらくOptionalなプロパティ(javaの変数名でdescription、revisionValue、libMap)の読み込みが続きますが、興味ないのでTargetの生成へ。

               AddOnTarget target = new AddOnTarget(addon.getAbsolutePath(), name, vendor,
                       revisionValue, description, libMap, baseTarget);

Vendor-idの読み込みを発見

               // get the USB ID (if available)
               int usbVendorId = convertId(propertyMap.get(ADDON_USB_VENDOR));
               if (usbVendorId != IAndroidTarget.NO_USB_ID) {
                   target.setUsbVendorId(usbVendorId);
               }

ADDON_USB_VENDORというプロパティ名で、値はconvertId()で正しく変換できる形式、と。

   /**
    * Converts a string representation of an hexadecimal ID into an int.
    * @param value the string to convert.
    * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the convertion failed.
    */
   private static int convertId(String value) {
       if (value != null && value.length() > 0) {
           if (PATTERN_USB_IDS.matcher(value).matches()) {
               String v = value.substring(2);
               try {
                   return Integer.parseInt(v, 16);
               } catch (NumberFormatException e) {
                   // this shouldn't happen since we check the pattern above, but this is safer.
                   // the method will return 0 below.
               }
           }
       }

       return IAndroidTarget.NO_USB_ID;
   }

convertId()では正規表現PATTERN_USB_IDSにマッチするものを取りますよと書いてます。
パターンはこちら。

    // usb ids are 16-bit hexadecimal values.
   private final static Pattern PATTERN_USB_IDS = Pattern.compile(
           "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE);

"^0x[a-f0-9]{4}$"なので、"0x"で始まり、16進の4桁の数字が続けば良いと。

今まで出てきたiniのプロパティ名の定数をまとめて引いてみると、

   private final static String ADDON_NAME = "name";
   private final static String ADDON_VENDOR = "vendor";
   private final static String ADDON_API = "api";
   private final static String ADDON_DESCRIPTION = "description";
   private final static String ADDON_LIBRARIES = "libraries";
   private final static String ADDON_DEFAULT_SKIN = "skin";
   private final static String ADDON_USB_VENDOR = "usb-vendor";
   private final static String ADDON_REVISION = "revision";
   private final static String ADDON_REVISION_OLD = "version";
   /** add-on manifest file */
   public final static String FN_MANIFEST_INI = "manifest.ini";

ということで、SDK/add-ons/の下に適当な名前でフォルダを作り、"manifest.ini"というテキストファイルを用意して、最初の結論に書いたように以下の形式で書き込んでみると・・・

name=Sony Tablet unofficial usb-vendor addon
vendor=tande
description=Adds USB support for Tablet S & P (Vendor id:0x054c)
api=11
revision=1
usb-vendor=0x054c

無事adb_usb.iniに書き込まれたのでした。
あーすっきりした。