eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

Android12対応でやったこと

もうすぐAndroid 12がリリースされるので、最低限対応した内容をメモする。

Android Studio AF対応

  • 新AndroidStudioにビルドツールを切り替えた

UnitTestをRobolectricからInstrumentation Testをメインに変更

  • Android Studio AFから、Robolectricが実行しづらくなった(できなくはないが、使いにくい)
  • なので、Instrumentation Testのみを行う方向に切り替えた
  • それに合わせてCIも変更した

Lint対応

  • Android 12でLint系が更新されたので、チクチクと変更

Activity.exported 属性の対応

  • AndroidManifest.xmlのActivityタグで、exported属性設定が必須になった
  • 内部・外部を問わず、aar内部で利用されているActivityにも適用されるので、ライブラリが古いとビルドが通らない場合がある
  • ビルドツールチェインがタコなので、問題が起きてももとのライブラリ名が不明

抽出

  • API 30の状態でapkをビルド -> 分解してAndroidManifest内のActivityを総チェック
  • exported属性がついていないActivityを洗い出して修正
  • androidx.test が内部で使用しているActivityも、古いバージョンだとexported属性が付いてないのでバージョンを更新

PendingIntentのフラグが厳密化されたので対応

  • androidx.work ライブラリはalpha版に切り替えないといけないので、stableが早く出てほしい

SONYのHDDレコーダーのリモート予約が正常動作しなくなったのでエンジニア的カンで直した

利用機器

  • SONY製 BDR-BDZ-FW2000(容量2TB, ホームサーバー機能対応のモデル。以下レコーダー)
  • マンション標準のネットワーク(グローバルIPは割り当てられない, 1家庭500~800MBps程度の速度が最大)
  • Google Nest Wi-Fi(ただしレコーダーはハブを介して有線接続)

事象

  • レコーダーのリモート予約機能が「レコーダーがネットワークに繋がっていない」という旨のエラーを表示してしまった
  • SONYのQ&Aはすべて試したが改善しない
  • 利用価値の半分がリモート予約(外出先で子供が「コレ予約してよ!」って言うことが多い)

観察

  • レコーダーのネットワークチェックは正常
  • そもそもホームサーバー機能の一部であるリモート視聴は行えるので、ネットワークは接続できている
  • 無線・有線を切り替えたが共に問題ない
  • iOS / Android両方で同じ事象

考察

  • アプリの挙動を見ると、リモート予約(POST/PUT)系だけではなく、「予約済み番組一覧」のGETが行えない
  • アプリのレビューやTwitterの検索をしてみるが、一斉不具合は起きていない
  • レコーダー <--> SONYサーバー <--> アプリの経路のうち、レコーダー <--> SONYサーバーの部分に(俺の環境のいずれか)問題がある恐れが濃厚である

更なる考察

  • アプリ側 / 経路内のプログラムの問題として、Invalidな予約データを受け取る -> 例外吐いて停止している恐れがある
  • レコーダーは年単位で予約している番組(ガイアの夜明けとか)が存在している
  • 古い番組のDate系に問題があるのではないか?

行動

  • 一旦すべての予約データを(後で治せるようにメモして)削除する
  • レコーダーを再起動する
  • これで無事に復旧した
    • 正確なところはわからないが、サーバーやアプリ側のValidationに問題があったのではないだろうか?

GCPを使って子供関係の通知をLINEに統合する

なにをやったのか

  • 子供が保育園・小学校・学習塾に通っている
  • それぞれ、お知らせがWebに掲載される(日記とかも含む)
  • 個別連絡はメールで届く(学年単位の緊急連絡とか)
  • いちいち学校のウェブサイトで確認したりメールを検索するのが面倒なので、LINEに通知を統合した

LINEの通知はIFTTTを使う

  • もうデファクトみたいな感じのIFTTT
  • IFTTTのWeb Hook -> LINE通知を組み合わせることで簡単に行えるので省略

Web Hookへの流し込み戦略

スクレイピング

  • 保育園や小学校、当然ながらRSSのような便利ソリューションは存在しない
  • なので、Firebase Functionsでポーリングすることにした
  • Firebaseの認証処理がスキップできる(デフォルトでAdmin権限が割り当て)られるので、各種処理が楽になる
  • 基本的に httpトリガー を使って、任意のタイミングで呼び出せるようにする
    • 関数の言語はなんでもいいけど、自分はGolang 1.13を使用した。
    • スクレイピング処理には github.com/PuerkitoBio/goquery を使用。
    • Webサイトの構造は当然サイトごとに異なるので頑張って個別対応。
  • Firebase Functions自体にもスケジューリング機能はついているけど、 Cloud Scheduler でKickするほうが再設定やデバッグが楽なので、スケジューリングは設定していない
  • 学校のCMSに負荷をかけるのは本意ではない(し、そこまでWebページの更新頻度は高くない)ので、cronは1時間に1回行ってる.

キャッシュ処理

  • IFTTTのWebHookにはキャッシュがないので、単純にCron実行すると同じ通知が何度も流れてしまう
  • そのため、 Firestore に通知済みのキャッシュを放り込んで、キャッシュが存在したらHookをスキップするようにする

通知とキャッシュのサンプルコード

  • 通知のタイトル・本文・リンクURLの組み合わせをMD5化して、それをキャッシュキーにする
  • キャッシュヒットしたら、それは通知済みなのでスキップ
var firebaseApp *firebase.App
var firestoreClient *firestore.Client
var webhook iftttWebhook.IFTTT

func init() {
    if app, err := firebase.NewApp(context.Background(), nil); err != nil {
        panic(err)
    } else if fs, err := app.Firestore(context.Background()); err != nil {
        panic(err)
    } else {
        firebaseApp = app
        firestoreClient = fs
    }

    webhook = iftttWebhook.New("割り当てられたAPIキー")
}

func md5sum(s string) string {
    sum := md5.Sum([]byte(s))
    return hex.EncodeToString(sum[:])
}

func notifyWebPageLink(ctx context.Context, title string, body string, link string) {
    uid := md5sum(title + "/" + body + "/" + link)

    ref := firestoreClient.Collection("ifttt").Doc("web-notify").Collection("cache").Doc(uid)
    if _, err := ref.Get(ctx); status.Code(err) != codes.NotFound {
        log.Println(fmt.Sprintf("    * Exist Document: %v", ref.Path))
        return
    }
    log.Println(fmt.Sprintf("    * Put Document: %v", ref.Path))
    log.Println(fmt.Sprintf("    * Title: %v", title))
    log.Println(fmt.Sprintf("    * Body: %v", body))
    if _, err := ref.Create(ctx, map[string]interface{}{
        "created_at": time.Now(),
        "title":      title,
        "body":       body,
        "link":       link,
    }); err != nil {
        log.Println(fmt.Sprintf("Firestore write failed: %v", err))
    }

    webhook.Emit("通知ID", title, body, link)
}
// これをhttpトリガーでPublish
func CronHoikuenWeb(w http.ResponseWriter, r *http.Request) {
        うまいことスクレイピング()...
    w.WriteHeader(http.StatusNoContent)
}

// これをhttpトリガーでPublish
func CronShogakkouWeb(w http.ResponseWriter, r *http.Request) {
        うまいことスクレイピング()...
    w.WriteHeader(http.StatusNoContent)
}

GmailからIFTTTへの通知戦略

  • 塾も入退室時にメールで通知してくれる
  • 保育園や小学校は緊急連絡(遠足延期とか)がメールで来る
  • なので、メールもLINEにGOだ。

GASをcronで呼び出す

  • Firebase Functionsだと、自分のGmailへのアクセスが面倒(認証とか)
  • なので、Google Apps Scriptから通知を飛ばすことにした
  • GASはGmail APIを使うとOAuthが自動的に開始されるので、認証がらく。
    • 認証状態を保持したまま、Cronを走らせられる
    • 設定項目もユーザーフレンドリー(cron構文使わなくてよい)
  • メールはGoogleインフラなので、1分に1回ポーリングしても大丈夫そう
    • リアルタイム性を重視したいならpub/subに流して〜とかあるけど、面倒な割にそこまでする必要はない
  • gmail検索条件をそのまま利用できるので、 from:example@example.com newer_than:1h とかできるし、めちゃくちゃ複雑なクエリも作れる
    • けど俺はシンプルにfromと直近のn時間のみでクエリを投げた
    • LINE通知は嫁さんにも届くので、間違ってもFANZAの課金通知を飛ばすわけにはいかねぇんだよ!!
  • UrlFetchApp.fetch の投げ先は、Functionsで作ったhttpトリガー.
    • POSTリクエストでパラメータを受け取り、それをそのままさっきの notifyWebPageLink() に流すようにした
// Functions側はこう
func WebHookFromGmail(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    bodyBytes, err := ioutil.ReadAll(r.Body)
    if err != nil {
        panic(err)
    } else {
        log.Printf("read body: %v", string(bodyBytes))
    }

    var request map[string]string
    if err = json.Unmarshal(bodyBytes, &request); err != nil {
        panic(err)
    } else {
        log.Printf("parsed: %v", request)
    }

    title := request["title"]
    body := request["body"]
    link := request["link"]

    notifyWebPageLink(ctx, title, body, link)

    // ok
    w.WriteHeader(http.StatusNoContent)
}
// GAS側はこう。
function onCron() {
  notifyGmail("gmailの検索条件", "通知タイトル", "リンクURL")
}

function notifyGmail(query, from, link) {
  GmailApp.search(query).forEach(function(thread){
    thread.getMessages().forEach(function(message) {
      console.log("============================");
      console.log(message.getSubject());
      console.log(message.getBody());
      notifyExternalWebHook(from, message.getSubject() + "  ::  " + message.getBody(), link);
    });
  });
}


function notifyExternalWebHook(title, body, link) {
  var requestBody = JSON.stringify({
    title : title,
    body: body,
    link: link,
  });
  console.log(requestBody);
  var options =
  {
    method  : "POST",
    payload : requestBody,
  };
  UrlFetchApp.fetch("自分のエンドポイントURL", options);
}

最後に

  • セキュリティもくそもない書き捨てコードなので、何かあったら自己責任で頑張ろうね!
  • あと、IFTTTのWebHookにキャッシュつけてくんねかな!!

ビルドの並列化をしすぎてCPU占有率100%になってしまったAndroid Studioプロジェクトの対処

何が起きたか

  • モジュール分割や様々な工夫をしてビルド時の並列性を高めた
  • 高めすぎて、Ryzen 3950Xの 16コア 32スレッド の能力を超えた並列性を持ってしまった
  • その結果、Gradleのビルドを行うとすべてのCPUリソースを完全に食い尽くすようになった
    • ZoomとかDiscordとか、セッションが途切れるレベルで食い尽くす
    • メモリ空き容量は30GBくらいあるので大丈夫なはず

解決方法

  • /gradle.propertiesに次のオプションを加えて、プロセスの優先度を下げた
  • Gradle 5.0からのオプションらしい。デフォルトは normal が設定される。
org.gradle.priority=low

その他

  • まさか並列実行性でRyzenが遅れを取るとは思いもしなかった

Android Studio Arctic Fox Canary-11の所感

現状の利点

  • Jetpack Compose(Beta, production-ready)が使える

AS 4.1.xからの移行

  • Android Gradle Pluginバージョン変更
  • gradlewバージョン変更

基本的に素直に移行できた。

現状の問題点

Library Projectで問題が出る。 Application Projectでは使えているので、モノリシックなプロジェクト以外は導入がつらそう(Unit Testをgradlewからの実行するのであれば気合で乗り切れるかもしれない)

  • Robolectircが必ずfailする
  • Instrumentation TestをAndroid Studioから実行できない
    • だめくさい

まとめ

まだ人柱レベル。 ComposeはProduction Readyでも、ASはProduction ちょいツライ。 頑張れGoogle、応援してるぞ。

イカれた映画観客を紹介するぜ!!

持ち込んだペットボトルをごっきゅごきゅと音を立てて就活生風の男 / TOHOシネマズ上野 / ラブライブサンシャイン

  • TOHOシネマズは基本的に飲食物は持ち込み禁止のはず
  • ごっきゅごきゅと音を鳴らして飲んでクソうるせー
  • 人間ってあんな音を立てながら飲めるんだな

映画を観ながらソシャゲーする30代くらいの男 / TOHOシネマズ柏 / シン・エヴァンゲリオン

  • 映画はソシャゲーする場所じゃねぇ
  • 水筒に飲み物入れて持ち込むな、持ち込み禁止じゃボケ
  • テメーのコンボにもガチャにも興味ねぇ
  • 画面隠してるつもりか?気になるんじゃボケ
  • 映画のエンドロールはガチャする時間じゃねぇ
  • ガチャしたけりゃ外に出ろ、上映中にスマホ開くなボケ

LINEをしながら「広瀬すず!!」って叫びながら観る高校生4人組 / TOHOシネマズ流山おおたかの森 / バケモノの子

  • 俺の映画体験ぶっちぎりワースト1。
  • しかもグループで来たのに固まって観れなかったから、(俺の)席左右をはさんで会話をする
  • LINEで実況するなクズども
  • 二度と映画館に来るなクズども
  • こいつらのせいで出演者に広瀬すずの名前がいると「あいつらいるのか」とか思うようになった
  • 「映画を観に来る学生集団」が嫌いになった理由

現状とってる回避策

  • 可能な限り平日
  • 可能な限り郊外
  • 可能な限り駅近は避ける
    • 駅直結は学生集団が集まりやすい
  • 可能な限り映画の日は避ける
    • 安い日は学生集団が集まりやすい
  • 可能な限り追加料金の発生する上映回を選ぶ
    • 学生集団は追加料金を出してまで映画を観ない

それでも

  • シン・エヴァンゲリオン鑑賞を邪魔したソシャゲー男は↑を満たしても現れた
  • わざわざ4DX追加料金出したのに
    • 4DX払ってまでソシャゲーやりに来るなよボケ

以上だ!

docker-composeでgoogle/cloud-sdk:emulators(firestore emulator)を利用する

docker-compose記述

version: "3"

services:
  firestore-emulator:
    image: google/cloud-sdk:329.0.0-emulators
    ports:
      - 8004:8004
    command: gcloud beta emulators firestore start --host-port "0.0.0.0:8004"
  • これで docker-compose up で起動できる。
  • ポートは任意(8004は俺の環境)

試行錯誤で見つけた問題点(自分の環境の場合)

  • Docker / WSL2環境では --host-port 127.0.0.1:8004 で接続できた
  • Mac, Linuxでは接続できない
  • --host-port 0.0.0.0:8004 だと接続できた
  • どの場合でも、docker-composeのコンテナ間では通信できるはず