Android Studioの外にでよう

これはAndroid Advent Calendar 2023 19日目の記事です。

qiita.com

背景

Android StudioIntelliJ IDEAをベースにしたAndroidアプリを開発するために最適化された統合開発環境です。 その説明のとおり、Androidアプリを開発するためぞれぞれのリソースを適したエディタで閲覧・編集できるようになっています。

ただ一方でわれわれソフトウェアエンジニアが扱うツールはAndroid Studioだけではありません。 私個人の話であれば、RubyやTypescriptなどを書くときにはVSCode(Visual Studio Code)を使うことが多いです。 つまり、VSCodeにも慣れており、そのエディタのよさも知っています。

VSCodeのよいところ

よく私がVSCodeで使う機能に Multiple selections (multi-cursor) があります。矩形選択で複数行の編集を一度にしたり、同じキーワードを置き換えたりできます。

Android Studioにも矩形選択は可能ですが、モード切り替えを経て編集して、矩形選択モードを解除する、という手間になかなか慣れませんでした。そんなことから、こういった操作をしたい場合はVSCodeAndroidアプリのプロジェクトを開き該当のファイルを開いて編集するといった操作をしていました。 ファイルを開く手間が必要でしたが、Android Studioの矩形選択の100倍マシだったので、こうしていました。

こういうファイルがあったときに 100.dp120.dp にしたいなあと思うとき、置換するよりもMultiple selectionsを使うほうが気が楽なんですよね。

もっと楽になりたい

100倍マシとは言いましたが、納得しているわけではなく、もっと楽にAndroid StudioからVSCodeに移動できないかなーと思って探したら、「External Tools」というのがピッタリの機能だったので、使ってみました。設定を済ませれば Shift 2回でおなじみ Search Everywhere からVSCodeにスッと移動できます。

しかもこれ、ただ同じファイルをVSCodeで開いているだけではありません。Android Studioのテキストカーソル位置も同じままにVSCodeを開いています。

どうやって設定するの?

External Toolsの設定はこんな感じです。

この設定によりAndroid Studioで開いているAndroidアプリのプロジェクトを開きつつ、いま編集中のファイルのテキストカーソル位置も維持したままファイルを開けます。

主に Arguments に設定している $ で囲まれた部分は、マクロと呼ばれるもので、プロジェクトやエディタの状態を変数として設定できるものです。+ ボタンからいろんな変数を追加できるので参考にしてみてください。

おわりに

私がAndroid StudioからVSCodeを開くようになったのはほんの一例で、他に使用している外部ツールがあった場合、この設定をしておくととても便利にツールを行き来できるようになります。もし開発中に使うツールで往来が大変だなーと思うことがあれば、この機能を活用してみてください。

GmailからLINEにメッセージを「すぐに」届けるための技術

この記事はmhidakaが建立した Advent Calendar 2023の12日目記事です。

こんにちは @tomorrowkey です ひつじさんから特にメンションきていませんが、オッと思ったので書いてみます。

モチベーション

息子が小学生になり、学校からの連絡がメールで届くのですが、Gmailは有象無象のメールがくるので、まぁ気づけません。
うちはだいたいコミュニケーションのハブをLINEにおいており、通知もきちんと消化できている実績があるので、そこに通知すれば見逃さずに済みそうです。
というわけで、よく見かける GAS + LINE Notify で該当のラベルがついたらLINEに通知するようにしました。

初代LINE通知くん

仕組みとしては、GASのスケジュール実行によって定期的にGmail APIを叩いて、該当のラベルの新しいメールが存在するかどうかを確認し、もし新しいメールがあったら、LINE Notifyを叩くといったものになっています。

この仕組みは本当によく見かけるもので、検索してみると山程記事がヒットします。

www.google.com

これはパッと見すごくシンプルでよさそうですが、運用してみると困難な点がいくつか見つかります。

2つの困難な点

リアルタイム性がない

よりリアルタイム性を高めるためには定期的に実行されるペースを短くする必要があります。例えば10分のインターバルで繰り返し実行されるように設定した場合、最大で10分の遅延がおこりえます。

メールが届いたときに即時でLINEにも通知がくるのがベストですが、単純にこのインターバルの時間を短くしてしまうと、今度はGmail APIを叩きすぎてAPI Rate Limitに引っかかってしまいます。

つまりリアルタイムの通知は不可能で、いい塩梅を見つけなければならないのです。

すでに転送済みのメールかどうかわからない

このGASのスクリプトは定期的に実行され、Gmail APIを叩いてメール検索に該当するメールを転送します。しかし、そのメールは前回GASスクリプトを実行したときに転送済みのメールではないでしょうか?同じ内容のメールがLINEに通知されてはノイズになってしまうため、同じメールが繰り返し通知されないようにしなければなりません。

このアプローチとしては「GASスクリプトGoogle Spreadsheetとリンクし通知済みのメールのタイムスタンプを記録する」「繰り返し間隔をもとに検索クエリを工夫する」の2つがあります。

前者アプローチは確実性がありますが、スクリプトの実装が少し複雑になってしまい、メンテナンスが難しくなります。後者のアプローチは、実装の複雑度はあがりませんが、スクリプトの実行タイミングが不安定であることを考えると、取りこぼすメールがでてくる可能性があります。

2代目LINE通知くんを想像する

よくあるLINE通知のスクリプトを使うことでLINEへのメッセージ転送を運用していましたが、ベストな運用ができないことが見えてきたところで、どうやったら改善できるか考えてみました。

2つの困難を一挙に解決するアプローチ

作った仕組みを実際に運用してみると、設計のタイミングで見えてこない困難が見えてくるのは、よくあることです。大切なのはこれでダメだったとへこたれず、よりよいアプローチを考えることですね。ただ困難が見つかるだけではなく、その周りの利用している仕組みに対しても解像度があがっているので、解決するアプローチも見つけやすいので、がんばって考えます。

1代目の仕組みは抽象的にとらえると、Pull型の仕組みになっているため、スクリプトを定期的に実行しなければなりません。定期的に実行するがゆえ、転送したかどうかの管理をする必要があります。では、Pull型の仕組みをPush型の仕組みにできれば、リアルタイム性と転送管理の両方を解決できます。

Gmail APIをながめてみたり、他の人がどんな実装しているのか眺めながらうんうん唸っていたのですが、そもそもメールには古来よりPush型の仕組みがあるなと思い出しました。*1そう「メール転送」です。Gmailにもメール転送の仕組みがあり、メールフィルターの定義で特定のメールアドレスへメールを転送できます。

メール転送をどうやってWebリクエストに変換する?

GmailからPush型のイベントを発火できる見込みがたちました。あとは、このメールの転送をいかにLINE Notify APIにつなげるかです。メールの転送をうけつけることをプリミティブに行うならメールサーバをたてて、そこからフックできそうです。ですが、インフラから作っていては、実現したいことに対してオーバーテクノロジーなように感じます。

こういうときにはSaaSに頼ります。 Mailgun というメール送信のためのSaaSがあります。このサービスには実はメールを送るだけではなく、メールを受け付ける機能もついています。しかもそのメール受信をきっかけに特定のURLを叩くことができます。つまりMailgunを使えば、メール転送をHTTPリクエストに変換できそうです。*2

2代目LINE通知くんの仕組み

困難な点を改善できそうな目論見ができたので、全体像を描いてみます。

処理の流れが1方向にだけ進み、とてもシンプルなものになりました。

ここからは細かい設定やコードの実装例を挙げます。

GAS

依存の関係性からまずはGASスクリプトから作るとよいです。 実装例は次のとおりです。*3

function doPost(event) {
  const token = 'YOUR LINE NOTIFY TOKEN HERE'
  const mailBody = event.parameters['body-plain'][0]

  const url = 'https://notify-api.line.me/api/notify'
  const options = {
    method: 'post',
    payload: `message=\n${mailBody}`,
    headers: {
      Authorization: `Bearer ${token}`,
    }
  }

  UrlFetchApp.fetch(url, options)
}

注意点としては、MailgunからのHTTPリクエストを受け付ける必要があるので、WebAppとしてデプロイし、認証なしでアクセスできるように設定します。

デプロイ完了したときのURLを手元に控えておきます。

Mailgun

Mailgunではメール受信をHTTPリクエストに変換する設定をします。 新しい受信ルールを作成します。

さきほど控えておいたGAS WebAppのURLをForwardのテキストに入力します。

注意点としては、すでにMailgunを他のプロダクトに使用している場合、メール受信の条件(Expression)も設定し、通知すべきものとそうではないものを区別する必要があります。

Gmail

Gmailではメール転送を設定します。転送先のメールアドレスはMailgunで無料で割り当てられるドメインを使います。

メールアドレスのユーザー名にあたる部分は自由に決めてしまって問題ありません。 MailgunのExpressionを設定する場合はこのユーザー名も含めて条件を設定するとよさそうですね。

Gmailへの設定方法は公式ヘルプやさまざまなメディアで紹介されているので、そちらを参照してください。

support.google.com

さいごに

以上で2代目LINE通知くんの仕組みが完成しました。初代のよくみる構成と違ってメールSaaSの設定がある点が面倒ですが、そこにある課題を見事に解決できたので、とても満足しています。こういう「ちょっとうまくいかない」を技術で解決できたときの快感はいくつになってもいいもんだなと思いますね。特にメール転送という古来からある仕組みを活用できたところが個人的にポイント高かったです。

来年もこういったものづくりを続けていきたいなと思います。

*1:正確にはSlackへのメール通知の設定しているシーンを見て、思い出しました

*2:メール送信によく使われるSendGridにも同等の機能があるので、そちらでも実現できます。

*3:私は google/clasp を使いTypeScriptで書いていることもあり、このコードの動作は確認していません

herokuからcloneできないソースコードをサルベージする

太古の昔にデプロイしていたサービスでgit管理もしていなくて、でもherokuにはソースコードがあるはず…。といったコードをサルベージする方法を書き残します。
heroku remoteからgit cloneできればよかったんですが、できなかった(時間が経ちすぎた?)ので、この方法をとりました。

heroku run '/bin/bash -c "tar -czf /opt/app.tgz ./ && base64 /opt/app.tgz"' --app $HEROKU_APP > app.base64 && \
base64 -d ./app.base64 > app.tgz && \
mkdir -p app && tar -xzf app.tgz -C app

要はアーカイブにしたものをbase64でホストコンピュータにリダイレクション、そしてアンアーカイブしてるだけです。 ファイルが大きすぎる…ということであれば、.git を諦めればVCS管理下のファイルだけを取得することもできます。

heroku run '/bin/bash -c "git archive HEAD -o /opt/app.zip && base64 /opt/app.zip"' --app $HEROKU_APP > app.base64 && \
base64 -d ./app.base64 > app.zip && \
unzip app.zip -d ./app

dynoを起動する必要があるので、無料期間終了の11月27日までにクローズするサービスについてはサルベージしておきたいですね。
そもそもGithubにプッシュしとけっつー話よな

Android物理デバイスをミラーリングする2022

Androidエンジニアの開発のお供であるミラーリングの話。
私がAndroidエンジニアとして駆け出しのころは Android Screen Monitor に大変お世話になりました。
2022年ミラーリングしたいと思ったときになにを使えばいいのか。Genymotion を作っている Geneymobile が便利なツールを作っていたことを知りました。

github.com

コマンド叩くだけでMac*1からミラーリングできて、かつマウスやキーボードから操作ができるツールです。 デバイス側に特別なアプリをインストールする必要もなく、adbで接続されてさえいれば使いはじめることができる。コピペ操作もMacからそのまますることができるところが最高。

ただ一点だけ面倒だなと思ったのは、コマンドで実行しないといけないので、ターミナルのタブを一つ提供しないといけないことだ。
バックグラウンドで動いてくれるように & つけて起動することもできるけど、実行時のログがちらちらしてきて煩わしい。
ウインドウを前面に持ってきたいと思ったときにExposeしてクリックしないといけないことも面倒。

要するにAlfredからコマンド起動できて、かつ、すでに起動済みだったら前面に表示すればいいんだろうってことで、スクリプト書きました。

github.com

Alfred + Powerpackを使っていればnpm経由でインストールすることができます。
ちょっとまえから気になっていた alfy という Alfred workflow をインストールするための仕組みも使うことができて満足。

*1:マルチプラットフォームもがんばっていそうな雰囲気がありますが未確認

Charlesを快適に使うためにやっていること

Charles Web Debugging Proxy とても便利ですよね。

今でこそ、StethoFlipper などの、Androidアプリ側に仕込むライブラリでも事足りるようにもなってきましたが、通信内容の改ざんや特定のリクエストのみ他のサーバに転送したりなど、多岐にわたる機能がとても便利なため、どうしてもこれらのライブラリでは手が届かないところは、Charlesを使い続けています。

そもそも Charles って何なの?どう使うの?といった内容のブログは山程あるので、そういった方はそちらを見ていただくとして、私が紹介したいのは、Charlesをよりストレスフリーに使うためのテクニックをご紹介したいと思います。mitmproxy など、他のプロキシツールでも使えるテクニックです。

Charlesを使いながら面倒だなあと思うこと

Charlesを日常で使っていて面倒なことといえば、プロキシの設定じゃないでしょうか。AndroidWiFiのプロキシを設定しようと思った場合、設定アプリのWiFiの画面を表示して、いま接続しているWiFiのオプションを開き、プロキシを設定するモードにして、IPアドレスを設定して、ポート番号を指定して… となかなかステップ数が多く、モバイルの小さい画面でIPアドレスを打つのはなかなかストレスフルです。そもそもIPアドレスを設定するためには、プロキシサーバになる母艦のIPアドレスを知る必要があるので、ターミナルでifconfig とか打たないといけないですね*1

逆にプロキシを使いたくない時もまた面倒だなあと思うことがあります。プロキシの設定がされたままではAndroid側は通信をすることができないので、母艦でCharlesを立ち上げ続けておく必要があります。Charlesはそんなに軽いアプリケーションというわけでもなく、必要ないときには起動しておきたくないので、モバイル側のプロキシの設定を消す必要があります。 ここで消したら再び使いたいときに、また奥まった機能の小さな画面でIPアドレス打ったりしないといけません。

PAC

日々プロキシの設定をしたり消したりしているうちに、あるオプションに気づきました。

プロキシの自動設定
プロキシの自動設定

プロキシの種類を指定するところに「プロキシの自動設定」とあります。なんとも甘味な響きに胸を打たれ、なんなのかと調べました。 調べてみると、これはプロキシに関する設定を定義として配布できる仕組みだと分かりました。

ja.wikipedia.org

起源はNetscape時代から始まっており、なかなか歴史があるものなのですが、全然知りませんでした。 これを使えば小さい画面でポチポチプロキシの設定することから開放されそうです。

PACをどう書くのか

PACファイルはhttp(s) によってアクセスできる場所に置いておく必要があります。Amazon S3Google Cloud Storageが手軽で適当だと思いました。 そこに次のようなファイルを置くとプロキシが有効になります。

function FindProxyForURL(url, host) {
  if (isPlainHostName(host) || isInNet(host,"127.0.0.1", "255.255.255.255")) {
    return "DIRECT";
  } else {
    return "PROXY 192.168.10.2:8888; DIRECT";
  }
}

PACファイルはJavascriptで記述します。FindProxyForURL という関数によってプロキシを使うかどうかを判定します。 この記述内容であれば、「通信先がローカルホストでなければ 192.168.10.2:8888 をプロキシとして使う」という内容になっています。

逆にプロキシを使わない場合は、次のようなファイルにしておきます。

function FindProxyForURL(url, host) {
  return "DIRECT";
}

DIRECT という文字列を返すと、プロキシを使いません。

詳しい書式は割愛します。プロキシ自動設定ファイル - HTTP | MDN あたりが詳しいです。

あとは、このファイルにアクセスするためのURLをモバイル側のプロキシの設定として設定するだけです。

プロキシの設定
プロキシの設定

日常でPACを使うために

Amazon S3Google Cloud Storageなどにファイルを置くようにすればファイルの更新も容易ですが、IPアドレスを調べたり、ファイルを更新したりといった作業も面倒だと思い、ツールを作りました。

github.com

コマンド一つでPACファイルを通してプロキシの有効/無効を切り替えることができるツールです。 局所的にそこだけコマンドを紹介すると、次のようなコマンドになります。

$ pac enable # プロキシ有効化
$ pac disable # プロキシ無効化

詳しい pacコマンドの設定や使い方は次節に書きます。

pac コマンドの使い方

設定

pacコマンドを使うためには、まず設定をする必要があります。これは、pacファイルをどこに置いておくか、という設定なので、誰しもがやる必要があります。 例えば Amazon S3にファイルを置く場合は次のようになります。

---
s3:
  bucket: your-bucket-name
  path: /

だいたい読み取れるんじゃないかと思いますが、この内容で、 s3://your-bucket-name/yout-name.pac という場所にPACファイルができます*2。 自分の環境に合わせてバケット名やパスを設定できます。

プロキシの有効化

次のコマンドで有効になります。

$ pac enable

内部で aws コマンドを使っているため、あらかじめ認証が通るようにしておく必要があります。

プロキシの無効化

$ pac disable

補足

  • Google Cloud Storageのことも書きましたが、実はサポートしていません。自分が使う範囲で使えるなーと思ったので、公開しました。気が向いたらGCSを対応するかもしれませんが、PR貰えれば嬉しいです。
  • Androidを中心に例を挙げましたが、もちろんiOSでも使うことができます。

最後に

PACファイルという存在を知ってから、プロキシを設定しなくてはならない、解除しなくてはならない、といったシチュエーションでだいぶストレスが減りました。通信内容を確認したいと思ったときに手軽にそれを使うことができるのは、とてもよいものですね。気持ちよく開発をすることができます。

*1:毎回この設定をしなくてはならない、というわけではありません。DHCPによって概ね同じIPがわりふられるようになっているんじゃないかと思うので、設定しなくていいシチュエーションの方がほとんどです。

*2:yout-name.pac にはログインユーザーの名前が入ります

通知センターの通知を全部消す

通知の種類をアラートからバナーに変更するとすぐに消えてしまって見逃しがありそう…ってことで、だいたいの通知はアラートにして、自動的に消えないようにしています。 ですが、ちょっと離席したときに通知が溜まっていると、それを全部ポチポチ消すのは面倒ですね。

そこで全部消すためのスクリプトをAlfred workflowにしています。 動作イメージはこんな感じ。

f:id:tomorrowkey:20180818144621g:plain

シュッと消えてくれて便利です。 こちらからダウンロードできますが、macOSの設定を英語にしていないと動きません。

github.com

ここのClose閉じるに変更すると日本語でも動くので、その場合はビルドしなおして alfredworkflowファイルを作り直してください。

https://github.com/tomorrowkey/alfred-workflows/blob/master/clear_notification/clear.applescript#L5

cloneしていないリポジトリのmasterのハッシュを調べる

git-ls-remote を使う

$ git ls-remote https://github.com/tomorrowkey/adb-peco master
788d83985e44f747a785cb948ac75fe4a048862d    refs/heads/master

もちろんgithubじゃなくてもいいからandroidのframework/baseのmasterを取得する場合はこんな感じ

$ git ls-remote https://android.googlesource.com/platform/frameworks/base master
d68f003c67ca6364202a57f0c695012d4ea4571e    refs/heads/master

オプションなど

$ man git-ls-remote
GIT-LS-REMOTE(1)                                                                                                                                              Git Manual                                                                                                                                             GIT-LS-REMOTE(1)

NAME
       git-ls-remote - List references in a remote repository

SYNOPSIS
       git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]
                     [-q | --quiet] [--exit-code] [--get-url] [--sort=<key>]
                     [--symref] [<repository> [<refs>...]]

DESCRIPTION
       Displays references available in a remote repository along with the associated commit IDs.

OPTIONS
       -h, --heads, -t, --tags
           Limit to only refs/heads and refs/tags, respectively. These options are not mutually exclusive; when given both, references stored in refs/heads and refs/tags are displayed.

       --refs
           Do not show peeled tags or pseudorefs like HEAD in the output.

       -q, --quiet
           Do not print remote URL to stderr.

       --upload-pack=<exec>
           Specify the full path of git-upload-pack on the remote host. This allows listing references from repositories accessed via SSH and where the SSH daemon does not use the PATH configured by the user.

       --exit-code
           Exit with status "2" when no matching refs are found in the remote repository. Usually the command exits with status "0" to indicate it successfully talked with the remote repository, whether it found any matching refs.

       --get-url
           Expand the URL of the given remote repository taking into account any "url.<base>.insteadOf" config setting (See git-config(1)) and exit without talking to the remote.

       --symref
           In addition to the object pointed by it, show the underlying ref pointed by it when showing a symbolic ref. Currently, upload-pack only shows the symref HEAD, so it will be the only one shown by ls-remote.

       --sort=<key>
           Sort based on the key given. Prefix - to sort in descending order of the value. Supports "version:refname" or "v:refname" (tag names are treated as versions). The "version:refname" sort order can also be affected by the "versionsort.suffix" configuration variable. See git-for-each-ref(1) for more sort options,
           but be aware keys like committerdate that require access to the objects themselves will not work for refs whose objects have not yet been fetched from the remote, and will give a missing object error.

       -o <option>, --server-option=<option>
           Transmit the given string to the server when communicating using protocol version 2. The given string must not contain a NUL or LF character. When multiple --server-option=<option> are given, they are all sent to the other side in the order listed on the command line.

       <repository>
           The "remote" repository to query. This parameter can be either a URL or the name of a remote (see the GIT URLS and REMOTES sections of git-fetch(1)).

       <refs>...
           When unspecified, all references, after filtering done with --heads and --tags, are shown. When <refs>... are specified, only references matching the given patterns are displayed.

EXAMPLES
           $ git ls-remote --tags ./.
           d6602ec5194c87b0fc87103ca4d67251c76f233a        refs/tags/v0.99
           f25a265a342aed6041ab0cc484224d9ca54b6f41        refs/tags/v0.99.1
           7ceca275d047c90c0c7d5afb13ab97efdf51bd6e        refs/tags/v0.99.3
           c5db5456ae3b0873fc659c19fafdde22313cc441        refs/tags/v0.99.2
           0918385dbd9656cab0d1d81ba7453d49bbc16250        refs/tags/junio-gpg-pub
           $ git ls-remote http://www.kernel.org/pub/scm/git/git.git master pu rc
           5fe978a5381f1fbad26a80e682ddd2a401966740        refs/heads/master
           c781a84b5204fb294c9ccc79f8b3baceeb32c061        refs/heads/pu
           $ git remote add korg http://www.kernel.org/pub/scm/git/git.git
           $ git ls-remote --tags korg v\*
           d6602ec5194c87b0fc87103ca4d67251c76f233a        refs/tags/v0.99
           f25a265a342aed6041ab0cc484224d9ca54b6f41        refs/tags/v0.99.1
           c5db5456ae3b0873fc659c19fafdde22313cc441        refs/tags/v0.99.2
           7ceca275d047c90c0c7d5afb13ab97efdf51bd6e        refs/tags/v0.99.3

SEE ALSO
       git-check-ref-format(1).

GIT
       Part of the git(1) suite

Git 2.18.0                                                                                                                                                    06/21/2018                                                                                                                                             GIT-LS-REMOTE(1)