Oculus Questのブラウザを自分好みに拡張する

Android版のChromeは拡張機能をインストールできないので，操作を自動化したり，サイトにちょっとした機能追加をしたいときに不便です．ChromiumをベースにしているOculus(Meta) Questのブラウザも同様の問題があります．

最低限，動画サイトで倍速再生したり，広告をブロックしたり，ファイルをGoogle Driveに保存したりできないと不便なので，[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) を使って特定のURLにアクセスした時にJavaScriptを実行するツールを書いたら便利になりました．

やったことは単純でこれといって面白い部分もありませんが，思ったより面倒だったのでまとめておきました．

# GoでDevTools Protocolを使う

Goで実装するため[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) を使うためのライブラリを探すと[chromedp](https://github.com/chromedp/chromedp)が有名なようでした．

https://github.com/chromedp/chromedp 

簡単に使えますが，ブラウザ上のテスト自動化にフォーカスしているみたいで，ユーザが操作しているブラウザに接続して何かするのは少し面倒が多い印象です．

### タブが勝手に閉じてしまう

`chromedp.WithTargetID(id)` で動作中のタブにattachしているときにcontextがcancelされると接続しているタブも閉じてしまうようです．エラー時にゴミを残さないためだと思いますが，使ってるタブを勝手に閉じられると困ります．

タブを閉じずに終了したりtarget(タブ)から簡単にdetachする方法が見当たらないので，少し強引ですが，切断前にtarget idをcontextから消すことで解決しました．

```go
if cc := chromedp.FromContext(ctx); cc != nil && cc.Target != nil {
    cc.Target.TargetID = ""
}
```

### タブにattachせずブラウザに接続する

`chromedp.ListenBrowser` でブラウザのイベントを監視できますが，基本的にタブに接続する方針になっているようでタブを閉じると接続が切れます．

タブが終了するたびに再接続するのは避けたいのでattachせずに使いたいのですがchromedpのAPIにそれらしき説明がありません．

APIを見ていると `chromedp.Targets()` がタブではなくブラウザに接続する唯一の機能のようです．

```go
// initialize cc.Browser
_, err := chromedp.Targets(ctx)
if err != nil {
    return err
}
cc := chromedp.FromContext(ctx)
target.SetDiscoverTargets(true).Do(cdp.WithExecutor(ctx, cc.Browser))
```

これも少しトリッキーな感じがしますが，`chromedp.Targets()` でブラウザに接続した後，`target.SetDiscoverTargets`でイベントを受け取れるようにしました．

chromedpの想定しているユースケースと違うようなので，chromedp/cdproto パッケージを使って自分で接続を管理したほうが良いのかもしれません．

### クラッシュする

chromedpはあまり長時間動作させるようには実装されていないみたいで，数日間動かしていると何度かクラッシュしました．問題の箇所を見ているとgoroutine間で共有しているmapをロックせずに使っていて稀に問題が起きるのは割り切って作られている感じがします．時間があれば直したいですが，とりあえずクラッシュしたら自動的に再起動することにして逃げました．

## Android上のChromeに接続したい

chromedpにDevToolsに接続するためのURLを渡す必要がありますが，Android上のChromeに接続するためには，ADBで接続する必要があります．

調べると`adb forward`でデバイス上のドメインソケットをTCPで接続できるようにする説明が見つかりますが，たとえlocalhost内でも認証無しでChromeのDevToolsにアクセスできる状態にしておくのは不安になります．デバッグ目的で一時的に有効にするなら気にしないですが，今回は常に動かしておく予定なので避けました．

Chromeのリモートデバッグ機能(`chrome://inspect`)はADBにも対応しているので，同様にADBで接続できるようにするのが良さそうです．

## GoでADB接続する

GoでADB通信をする使いやすいライブラリが見つからなかったので，[ADBのプロトコルの説明](https://android.googlesource.com/platform/packages/modules/adb/+/master/protocol.txt)を見ながら実装してみました．単純なプロトコルですが，説明と違う実装になっていたり行間を読まないといけないので，概要だけつかんだら実装を読むほうが良いと思います．

https://github.com/binzume/adbproto

Dockerコンテナ上で動かす予定で，あまりAndroidのplatform-tools等に依存したくないので単体で動くように実装しました．

通常のADBコマンドはadb-serverを介してAndroidに接続していますが，今回はAndroid上のadbdと直接通信します．RSAの署名周りが少し面倒くさそうに見えますが，PKCS #1 v1.5で良いのでGoの標準のcrypto/rsaパッケージで対応できます．(PSSでも受け付けてくれるかもしれないですが確認していません)

```
adb command(PC) <-TCP-> adb-server(PC) <-USB/TCP-> adbd(Android)
```

```
adbproto(Go) <-TCP-> adbd(Android)
```

あとは，chromedpが接続に使っている `ws.DefaultDialer` を実行時に上書きしてWebSocketに接続しようとしたときにADB経由でAndroidのドメインソケットに接続するようにしたら，無事にブラウザに接続できました．

# ブラウザを監視して特定サイトを開いた時にJavaScriptを実行するツール

Chrome DevTools Protocolでブラウザを監視して，スクリプトを実行する簡単なプログラムを書きました．

https://github.com/binzume/chrome-watch

在りし日のGreasemonkeyとよく似たフォーマットのスクリプトを読み込めます．
たとえば，以下のようなスクリプトファイルを置いておけば，`www.binzume.net`というサイトにアクセスした時にページのテキストが赤くなります．

```js
// ==UserScript==
// @name         RedText
// @match        https://www.binzume.net/*
// ==/UserScript==

let styleEl = document.createElement("style");
styleEl.textContent = "body{color:red !important}";
document.head.appendChild(styleEl);
```

HttpOnly 属性付きの Cookie にアクセスする機能とかもあるので大抵のことは出来ると思います．

プログラムはLAN内の別マシンで動かしているので家のWiFiに繋いでいるときしか有効になりませんが，基本的に家にいるのとOculus Quest2で使うのが目的なので問題ありません．

# Oculus(Meta Quest) BrowserのUIをカスタマイズする

Oculus BrowserはブラウザのUIがHTMLで実装されているのでブラウザに組み込まれたページを変更することで見た目や動作を変更できます．

- `chrome://panel-app-nav/` ブラウザUI
- `chrome://oculus-ntp/` 新規タブページ
- `chrome://oculus-ntp-private/`  新規タブページ(プライベートモード)

ブラウザの挙動については過去に書いた以下の記事も参考になるかもしれません．

https://qiita.com/binzume/items/49525e8dbfbabde2e708

例えば`chrome://panel-app-nav/*`のページの内容を変更するとでメニューの内容をカスタマイズしたり，ブラウザの見た目を変更できます．

![oculusbrowser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58515/dc627ea5-8826-eeca-e557-10219b021c6f.png)
サンプルとして，[ブラウザのUIの色を変えるスクリプト](https://github.com/binzume/chrome-watch/tree/master/scripts)を入れておきました．分かりにくいですが，ブラウザのタブなどのUIが半透明の青色になっています．