Docker for Macを使って複数のAndroid端末に並列でADBで何かをする

自分用メモ。


Docker for Mac でコンテナからホスト側に接続したAndroidデバイスにadbする方法
https://qiita.com/YusukeIwaki/items/5d31c10bc9bfe6d62845

を少し発展させて、２つのDockerコンテナから２つの端末にADB接続するようにしたい。

![複数ADB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/b39e90ac-e9f7-464a-32d7-88d818987a28.png)

---

## `-s` オプションと同様のことを実現する環境変数 ANDROID_SERIAL

普段、複数端末をPCにさしている場合などは `adb -s E32094823 shell` のように -sでデバイスシリアルIDを指定することが多い。これをDockerでやりたいわけだが、できればこういうのは環境変数で持たせたい。

```
$ adb
Android Debug Bridge version 1.0.41
Version 29.0.1-5644136
Installed as /Users/yusuke-iwaki/Library/Android/sdk/platform-tools/adb

global options:
 -a         listen on all network interfaces, not just localhost
 -d         use USB device (error if multiple devices connected)
 -e         use TCP/IP device (error if multiple TCP/IP devices available)
 -s SERIAL  use device with given serial (overrides $ANDROID_SERIAL)
 -t ID      use device with given transport id
 -H         name of adb server host [default=localhost]
 -P         port of adb server [default=5037]
 -L SOCKET  listen on given socket for adb server [default=tcp:localhost:5037]
```

`-s SERIAL  use device with given serial (overrides $ANDROID_SERIAL)`

お！？

ANDROID_SERIAL？

```android.googlesource.com/platform/system/core/+/master/adb/client/commandline.cpp
    // If none of -d, -e, or -s were specified, try $ANDROID_SERIAL.
    if (transport_type == kTransportAny && serial == nullptr) {
        serial = getenv("ANDROID_SERIAL");
    }
```

-s で何も指定されていなかったら、 `getenv("ANDROID_SERIAL")` で代入しているので、やはり `-s` と同じ効果がありそうだ。

## docker-composeで。

```docker-compose.yml
version: "3"
services:
  workspace_hogeperia:
    image: your_own/adb
    command: adb logcat
    environment:
      - ANDROID_SERIAL=CB1A2B3C45

  workspace_fugafone:
    image: your_own/adb
    command: adb logcat
    environment:
      - ANDROID_SERIAL=X51223098765
```

こんな感じで環境変数で分けておけば、 `docker-compose up` すれば

```
workspace_fugafone_1   | 07-19 16:27:44.972  1455  2348 E ActivityManager:     at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:240)
workspace_fugafone_1   | 07-19 16:27:44.972  1455  2348 E ActivityManager:     at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3026)
workspace_fugafone_1   | 07-19 16:27:44.972  1455  2348 E ActivityManager:     at android.os.Binder.execTransact(Binder.java:674)
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager: Sending non-protected broadcast jp.co.sharp.android.keyguard.action.LOCKSCREEN_TOUCH_EVENT from system 1728:com.android.systemui/u0a48 pkg com.android.systemui
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager: java.lang.Throwable
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:19258)
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.broadcastIntentLocked(ActivityManagerService.java:19863)
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.broadcastIntent(ActivityManagerService.java:20005)
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager:     at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:240)
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3026)
workspace_fugafone_1   | 07-19 16:27:44.989  1455  7369 E ActivityManager:     at android.os.Binder.execTransact(Binder.java:674)
workspace_hogeperia_1  | 08-21 16:42:57.992   652   841 E PPDaemon: void ScreenRefresher::ProcessRefreshWork(): pthread_cond_timedwait failed. err: Success
workspace_hogeperia_1  | 08-21 16:42:57.993   652   841 E PPDaemon: void ScreenRefresher::ProcessRefreshWork(): pthread_cond_timedwait failed. err: Success
workspace_hogeperia_1  | 08-21 16:42:57.995   652   841 E PPDaemon: void ScreenRefresher::ProcessRefreshWork(): pthread_cond_timedwait failed. err: Success
workspace_hogeperia_1  | 08-21 16:42:57.996   652   841 E PPDaemon: void ScreenRefresher::ProcessRefreshWork(): pthread_cond_timedwait failed. err: Success
workspace_hogeperia_1  | 08-21 16:42:57.999   652   841 E PPDaemon: void ScreenRefresher::ProcessRefreshWork(): pthread_cond_timedwait failed. err: Success
workspace_hogeperia_1  | 08-21 16:42:58.027  7445  7496 W [Photo Analyzer]: face provider: storage is not granted.
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils: Writing exception to parcel
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils: java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaProvider uri content://media/external/extended_images/media from pid=7445, uid=10085 requires android.permission.READ_EXTERNAL_STORAGE, or grantUriPermission()
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils:   at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:608)
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils:   at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:483)
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils:   at android.content.ContentProvider$Transport.query(ContentProvider.java:212)
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils:   at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:118)
workspace_hogeperia_1  | 08-21 16:42:58.034 32045 32060 E DatabaseUtils:   at android.os.Binder.execTransact(Binder.java:565)
workspace_hogeperia_1  | 08-21 16:42:58.038  7445  7496 W [Photo Analyzer]: [MessageHandlerThread]processFaceRecognition() please check permission:Permission Denial: reading com.android.providers.media.MediaProvider uri content://media/external/extended_images/media from pid=7445, uid=10085 requires android.permission.READ_EXTERNAL_STORAGE, or grantUriPermission()
workspace_fugafone_1   | 07-19 16:27:45.022  1455  7369 I chatty  : uid=1000(system) Binder:1455_16 identical 2 lines
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager: Sending non-protected broadcast jp.co.sharp.android.keyguard.action.LOCKSCREEN_TOUCH_EVENT from system 1728:com.android.systemui/u0a48 pkg com.android.systemui
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager: java.lang.Throwable
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:19258)
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.broadcastIntentLocked(ActivityManagerService.java:19863)
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.broadcastIntent(ActivityManagerService.java:20005)
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager:     at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:240)
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3026)
workspace_fugafone_1   | 07-19 16:27:45.037  1455  7369 E ActivityManager:     at android.os.Binder.execTransact(Binder.java:674)
workspace_fugafone_1   | 07-19 16:27:45.043   530   582 I NfcNciHal: nci_hal_lp_timeout_cback ()
workspace_fugafone_1   | 07-19 16:27:45.043   530   582 I NfcNciHal: nfc_hal_dm_power_mode_execute () event = 2 p=0 sz=8 kw=0
workspace_fugafone_1   | 07-19 16:27:45.043   530   582 I NfcNciHal: nfc_hal_dm_set_nfc_wake () DEASSERT
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager: Sending non-protected broadcast jp.co.sharp.android.keyguard.action.LOCKSCREEN_TOUCH_EVENT from system 1728:com.android.systemui/u0a48 pkg com.android.systemui
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager: java.lang.Throwable
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:19258)
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.broadcastIntentLocked(ActivityManagerService.java:19863)
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.broadcastIntent(ActivityManagerService.java:20005)
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager:     at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:240)
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager:     at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3026)
workspace_fugafone_1   | 07-19 16:27:45.054  1455  7369 E ActivityManager:     at android.os.Binder.execTransact(Binder.java:674)
wo
```

２つのコンテナから２つの `adb logcat` が並列実行できたぞ、っと。

## ちなみにAppiumは一筋縄ではいかない

adbを素で使うケースだとわかりやすいんだけど、Appiumのように内部でadbをラップしてるようなライブラリを使う場合には注意が必要。

* appiumは内部でadbをラップしてなにかやっているので、ANDROID_SERIALを素直に読んでくれなかった。
    * 明示的にでdesired capのudidパラメータに環境変数のANDROID_SERIALを入れる、みたいなことをやる必要がある
* appiumは、こっそり `adb forward` を内部で実行して、Mac側のポートをバインドしてAndroidデバイス側のサーバーにコマンドを転送する仕組みをしているので、並列動作させるにはポート番号をずらす必要がある
    * desired capのsystemPortパラメータをコンテナごとに変える必要がある

　

内部でadb forwardしているというのは、確かに言われてみたらそうだよなーとは思うんだけど、ソース見るまで自分は気づかなかった。

```appium/appium-espresso-driver｜lib/driver.js
    // set up the modified espresso server etc
    this.initEspressoServer();
    // Further prepare the device by forwarding the espresso port
    logger.debug(`Forwarding Espresso Server port ${DEVICE_PORT} to ${this.opts.systemPort}`);
    await this.adb.forwardPort(this.opts.systemPort, DEVICE_PORT);
```

```appium/appium-uiautomator2-driver｜lib/driver.js
    // set up the modified UiAutomator2 server etc
    await this.initUiAutomator2Server();

    // Further prepare the device by forwarding the UiAutomator2 port
    logger.debug(`Forwarding UiAutomator2 Server port ${DEVICE_PORT} to ${this.opts.systemPort}`);
    await this.adb.forwardPort(this.opts.systemPort, DEVICE_PORT);
```

systemPortを同じにしてしまった時のエラーログが

```log:AppiumでsystemPortがかぶっている時のログ
workspace_fugafone_1  | 05:31:15 appium.1  | [debug] [WD Proxy] Matched '/status' to command name 'getStatus'
workspace_fugafone_1  | 05:31:15 appium.1  | [debug] [WD Proxy] Proxying [GET /status] to [GET http://docker.for.mac.localhost:8200/wd/hub/status] with no body
workspace_fugafone_1  | 05:31:15 appium.1  | [WD Proxy] Got an unexpected response: {"errno":"ECONNREFUSED","code":"ECONNREFUSED","syscall":"connect","address":"192.168.65.2","port":8200}
workspace_fugafone_1  | 05:31:16 appium.1  | [debug] [WD Proxy] Matched '/status' to command name 'getStatus'
workspace_fugafone_1  | 05:31:16 appium.1  | [debug] [WD Proxy] Proxying [GET /status] to [GET http://docker.for.mac.localhost:8200/wd/hub/status] with no body
workspace_fugafone_1  | 05:31:16 appium.1  | [WD Proxy] Got an unexpected response: {"errno":"ECONNREFUSED","code":"ECONNREFUSED","syscall":"connect","address":"192.168.65.2","port":8200}
workspace_fugafone_1  | 05:31:17 appium.1  | [debug] [WD Proxy] Matched '/status' to command name 'getStatus'
workspace_fugafone_1  | 05:31:17 appium.1  | [debug] [WD Proxy] Proxying [GET /status] to [GET http://docker.for.mac.localhost:8200/wd/hub/status] with no body
workspace_fugafone_1  | 05:31:17 appium.1  | [WD Proxy] Got an unexpected response: {"errno":"ECONNREFUSED","code":"ECONNREFUSED","syscall":"connect","address":"192.168.65.2","port":8200}
```

こんな感じなので、パット見でポートの取り合いになっているということにも気付きづらい。