CORDEA blog

Android applications engineer

Reports API で G Suite 監査ログの更新通知を受け取る

G Suite の Admin console には監査ログというものがあり、そこで色々見たり Alert を上げたりすることが出来るのですが、
G Suite Admin SDKReports API を使用すると、この監査ログを取得することが出来ます
また、Callback URL を登録することで更新通知を受け取ることも出来ます。

ということでこの記事では Meet の更新通知を受け取ってみます。
Meet は非対応ですが、主要なイベントだと Alert Center でメールに飛ばせるのでメールならそちらのほうが良いでしょう。

注意

監査ログはリアルタイムに通知されるわけではなく、利用できるようになるまでに一定の時間がかかります。また、この時間はサービスによって異なります。
Meet はほぼリアルタイムに更新されるようですが、私が見ていたところでは監査ログに表示されるまでに概ね 3~5 分程度かかっていました。

Data retention and lag times - G Suite Admin Help

また、更新通知の受け取りに関してですが、最初は届いていたのですが何度も試していたせいか途中から届かなくなってしまいました。(同期用の Message は届くが監査ログの更新は通知されない)
Stackoverflow でもいくつか同様の投稿が見受けられましたので、使用する際には注意したほうが良いかもしれません。

個人で契約している G Suite Basic なので、Business とか Enterprise だと違ったりするのかもしれません。

実装

実装したやつ置いときます

github.com

通知を受け取る Web API の用意

とりあえず通知が POST されるので、その先を予め用意しておく必要があります。

Flask だと以下のようになります。

@app.route('/events', methods=['POST'])
def events():
  headers = request.headers

if __name__ == '__main__':
  app.run(debug=True)

まず、ここにはいくつかの Message が POST されますが、そのうちの一つに同期用の Sync Message があります。

これは登録後に POST される確認用の Message です。
この Message には登録解除用の ID などが含まれていますが、監査ログとしての情報はないため、ここでは無視します。

@app.route('/events', methods=['POST'])
def events():
  headers = request.headers
  if headers['X-Goog-Resource-State'] == 'sync':
    return {}

Header に含まれる情報はここに書いてあります。

あとは通知の Parse です。
POST される json には種別や ID などが含まれますが、おそらく大体の人が必要としている情報は events にあります。
配下は配列となっており、その中身には Event type, name と Event の parameter が含まれています。
Event の parameter についてはそれぞれの Reference に記載があります。
Meet の場合は以下

Hangouts Meet Audit Activity Events  |  Reports API  |  Google Developers

あとは欲しい情報を取得してなんかいい感じにするだけです。

@app.route('/events', methods=['POST'])
def events():
  headers = request.headers
  if headers['X-Goog-Resource-State'] == 'sync':
    return {}
  for event in request.json['events']:
    print(event['name'])

Deploy する

さきほど用意した Web API をどこか見えるところに Deploy します。
Callback URL に登録するためにはいくつか条件があります。

  • https であること
    • 自己署名などは不可です
  • Search console で domain の所有権が確認できること

私の場合は Google App Engine に deploy しました。
Repository に app.yaml を commit しておいたので同じように試すときは gcloud app deploy で試すことが出来ます。

Domain を登録する

Google API Console にアクセスし、必要なら任意の Project を選択します (無いなら作る)。
Domain verification から Add domain を選択し
Callback URL の domain を入力します。
この時、所有権が確認できていない場合は Seach Console での domain 登録を求められますので、その通りにします。
私は meta tag で行ったので Repository に index.html.example として commit してあります。

Reports API を有効化する

おそらく Admin SDK という名前になっているのですが、これを有効化して credentials.json を持ってくる必要があります。

Python Quickstart  |  Reports API  |  Google Developers

Callback URL を登録する

Activities: watch  |  Reports API  |  Google Developers

あとは Callback URL を登録するだけです。
Python だったらこのへんを参考にします。

Python Quickstart  |  Reports API  |  Google Developers

def __get_google_service():
    creds = None
    if os.path.exists(TOKEN_PICKLE):
        with open(TOKEN_PICKLE, 'rb') as token:
            creds = pickle.load(token)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json',
                SCOPES
            )
            creds = flow.run_local_server(port=0)
        with open(TOKEN_PICKLE, 'wb') as token:
            pickle.dump(creds, token)
    return build('admin', 'reports_v1', credentials=creds)


def __register():
    response = __get_google_service().activities().watch(
        userKey='all',
        applicationName='meet',
        body={
            'address': settings.CALLBACK_URL,
            'type': 'web_hook',
            'id': ID
        }
    ).execute()

applicationName は今回は Meet の Event を取得するため 'meet' にします。Calendar の場合は 'calendar' です。
userKey は特定の User に絞りたい場合は使用します。今回は全て見たいので 'all' とします。

Response で得られる Resource ID と ID は保持するなり覚えておくなりすると登録解除が出来ます。
登録状態を見る API は無いため、Resource ID と ID がどこかへ行ってしまうと期限が切れる (たぶん 6 時間) のを待つ以外にありません。

登録するとしばらくして Sync Message が届くはずです。

登録を解除する

Channels: stop  |  Reports API  |  Google Developers

登録の解除は以下のように行います。

__get_google_service().channels().stop(
    body={
        'id': id,
        'resourceId': resource_id
    }
).execute()

が、どうやら Python の Client library だとこの stop API の URL が間違っているようなので Issue を参考に書き換えます。

channels._baseUrl = channels._baseUrl.replace('/admin/reports/v1', '')

なお Issue では AuthorizedSession を使う方法も提案されていて、そちらのほうが良さげです。


おわり

EMV Contactless 対応のクレジットカードから情報を読み取る

クレジットカードやデビットカードには EMV Contactless という非接触決済に対応しているものがあります。

EMV Contactless は NFC を使用しているため、手持ちの Android 端末でカードの情報を読み取る事ができます。
ということでここではカードからカード番号及び有効期限を読み取ってみます。

なお、触れると長くなるのであまり具体的な実装には触れません。Android での実装は下部に Repository の URL を貼っておくので興味があればそちらを見てください。

はじめに

情報を読み取るために、ISO/IEC 14443-4 に定義されている通信プロトコルを介して通信を行います。データへのアクセスには ISO/IEC 7816-4 で定義されている Application Protocol Data Unit (APDU) という通信フォーマットを使用します。

雑に Android での実装の話をすると、IsoDep を使って connect で操作を開始し、transceive でやり取りし、close で終了します。

APDU Commands

Command は以下のように構成されます。

| CLA | INS | P1 | P2 | Lc | Data | Le |

Lc には Data length、Le には Response length を入れます。Response length は Response をすべて取りたい場合は 0 にします。
例えばこのあと出てくる SELECT FILE command の場合

| 00 | A4 | 04 | 00 | Lc | Data | Le |

となります。
P1/P2 は Parameter です。P1 の 04 (00000100) はこの場合 Dedicated File (DF) をその名前によって選択するということになります。詳しくは以下の Table 58 を見てください。

また、Response は以下のように構成されます。

| Data | SW1 | SW2 |

SW1-SW2 で Status を表します。90 00 が成功ステータスです。詳しくは以下の Table 17,18 を見てください。

読み取り

本編です

PSE を選択する

SELECT FILE Command を送信し、Payment System Environment (PSE) を選択します。
PSE の DF Name は "1PAY.SYS.DDF01"、PPSE の場合は "2PAY.SYS.DDF01" です。
サポートしていれば PSE の File Control Information (FCI) が返却されます。これに Application Elementary Files (AEF) の Short File Identifier (SFI) が含まれています。

AEF のレコードを読む

PSE をサポートしているカードのみの手順です。

SFI を使用して READ RECORD Command を送信し、Application Identifier (AID) を取得します。

AID でファイルを選択する

PSE をサポートしていた場合は先に取得した AID を、していなかった場合は直接 AID を指定し選択します。

例えば、Visa のクレジットカード・デビットカードは A0 00 00 00 03 10 10 です。

以下のように SELECT FILE Command を送信します。

| 00 | A4 | 04 | 00 | 07 | A0 00 00 00 03 10 10 | 00 |

Response として Processing Options Data Object List (PDOL) が返却されます。

PDOL を Parse する

返却された PDOL を Parse します。
PDOL は概ね以下のようになります。

 ... 01 01 9F 38 18 9F 66 04 9F 02 06 9F 03 06 9F 1A 02 95 05 5F 2A 02 9A 03 9C 01 9F 37 04 ...

PDOL の開始 Tag は 9F 38 なので、それを探します。

また、開始 Tag の直後は length です。9F 38 18 であれば length は 24 となります。
このことから PDOL は以下のようになります。

9F 66 04 9F 02 06 9F 03 06 9F 1A 02 95 05 5F 2A 02 9A 03 9C 01 9F 37 04

この PDOL は次に送る Command に request として何をどの length で送ればよいかを示しています。

  • 9F 66
    • Terminal Transaction Qualifiers (TTQ)
  • 9F 02
    • Amount, Authorised
  • 9F 03
    • Amount, Other
  • 9F 1A
    • Terminal Country Code
  • 95
    • Terminal Verification Results (TVR)
  • 5F 2A
    • Transaction Currency Code
  • 9A
    • Transaction Date
  • 9C
    • Transaction Type
  • 9F 37
    • Unpredictable Number

以上の Data を指定された length で構築します。
以下はそれぞれの例です。実際はそれぞれの要件等に合わせて構築します。

できたらこれらをくっつけます。

GPO Command を送信する

GET PROCESSING OPTIONS (GPO) Command を送信します。
先程の Data の length を先頭に付けます。

21 28 00 00 ...

Tag (83) をつけます。

83 21 28 00 00 ...

以下のように送信します。

| 80 | A8 | 00 | 00 | 23 | 83 21 28 00 ... | 00 |
Track2 Equivalent Data を Parse する

カード情報が含まれている Data を Parse します。Response として Track2 Equivalent Data が返却されている場合、開始 Tag は 57 です。
返却されていない場合はこちらではなく次を見てください。
57 の直後は length で、delimiter は 'd' です。

以下のようにデータが含まれています。

xx xx ... xx dy yy ...
  • x = カード番号
  • y = 有効期限
AFL を Parse する

ここから最後までは Track2 Equivalent Data ではなく Application File Locator (AFL) が返却された場合の手順です。

AFL の開始 Tag は 94 です。
Data から SFI と start, end の 3 bytes を取得します。

レコードを読む

さきほどの SFI をもとにすべてのレコードを読みます。
P1 として index (start から end まで), P2 として下位 3 bits を 1 0 0 とした SFI をセットします。これは P1 のレコードを読むという意味です。以下の Table 36 を見てください。

Track2 Data を Parse する

返却された Track2 Data を Parse します。開始 Tag は 9F 6B です。
あとは Track2 Equivalent Data と同じです。

終わりに

以上で読み取りまでの流れは終わりです。
結構端折った部分や私自身の知識が甘い部分もあり、調べながらでないとこの記事を読んで実装することは難しいと思いますが、私が実装するときに情報が散らばっていたりしてかなり苦労したので書いておきました。
以下に参考にしたリンクを貼っておきます。すべて非常に参考になるので読んでみてください。

あと実装した Repository も置いておきます。EMV Contactless 対応の Visa と Mastercard 3 枚で動作を確認していますので興味があれば。

github.com

Android で Liquid-like な animation を Path#cubicTo で実現する

Dribbble にありがちなスライムのような何かぽよぽよした Animation をどう実現するか

ちなみに特に何かイベントに反応する必要がなければ Lottie で良いと思います

https://raw.githubusercontent.com/CORDEA/Poyo/master/images/slime.gif
https://raw.githubusercontent.com/CORDEA/Poyo/master/images/circle.gif

Path#cubicTo

実現する方法として、結論としてはタイトルにある通り Path#cubicTo を使います
これで Bezier curve を描くことができるので、柔らかい曲線や遷移を実現できます。

以下のような感じで引いていきます。

path.moveTo(0f, 0f)
path.cubicTo(0f, 0f, 0f, 0f, 0f, 0f)
path.cubicTo(0f, 0f, 0f, 0f, 0f, 0f)
path.cubicTo(0f, 0f, 0f, 0f, 0f, 0f)
path.close()

一つ注意点として、cubicTo の x1, y1 は point の左側の control point ではないので、
以下のような曲線を引く時

f:id:CORDEA:20200426121908p:plain:w500


x1, y1 は前の point の右側の control point を指します。

f:id:CORDEA:20200426121946p:plain:w500

作ってみる

これだけだと cubicTo の説明にしかなっていないので、作るときに楽だった方法だけ書いておきます。
まず Sketch や Figma などまぁそのへんの色々を使用して Animation のあたりをつけておきます。
先にあった gif の一番目だとこのような感じで作っていました。

f:id:CORDEA:20200426122611p:plain:w500

f:id:CORDEA:20200426122630p:plain:w500

こうするとだいたい progress 0.0 ~ 1.0 までどのように control point を動かしていけば想定どおりの動きとなるかが見えてきます。
まず progress 0.0 と 1.0 の状態 (もっと状態が複雑ならそれら) を作って、大体出来たらそれを補完するように point を動かします。
やることはこれだけです。

なお、作るときには control point も同じく描画しておくと良いでしょう。今は Android Studio 上で View の preview が見られるので調整作業がかなり楽になります。

f:id:CORDEA:20200426123234p:plain:w500

終わりに

完全な円は書けないので、円から何かするというのは難しいかもしれません。
あと、Animation の見え方が device によってかなり変わると思うので、お仕事でやるなら相当苦しむことになると思います。パフォーマンス的にもとてもおすすめできない。
作るだけならまぁまぁ楽しいので、一回やってみると良いかもしれません。

上の Animation の実装は以下の Repository にあります。とても参考にできない実装ですが興味があれば。

github.com

Jetpack Compose の @Model で生成されるコード

メモ

こういうの書いたときに生成されるコードが気になっていた

@Model
class AnimalState(
    var isAnimal: Boolean,
    var cats: List<Cat>,
    var dog: Dog
)

@Model
class Cat(
    var isRunning: Boolean,
    var isWalking: Boolean
)

@Model
class Dog(
    var count: Int,
    var type: Type
) {
    enum class Type {
        BULLDOG,
        DACHSHUND,
        SHIBA
    }
}

build 配下を見て Kotlin で書き直すと概ねこんな感じになりそう

class AnimalState(
    isAnimal: Boolean,
    cats: List<Cat>,
    dog: Dog
) : Framed {
    private var record: Record = Record()

    private val readable
        get() = _readable(
            record,
            this
        ) as Record

    private val writable
        get() = _writable(
            record,
            this
        ) as Record

    var isAnimal: Boolean
        get() = readable.isAnimal
        set(value) {
            writable.isAnimal = value
        }

    var cats: List<Cat>
        get() = readable.cats
        set(value) {
            writable.cats = value
        }

    var dog: Dog
        get() = readable.dog!!
        set(value) {
            writable.dog = value
        }

    init {
        record.isAnimal = isAnimal
        record.cats = cats
        record.dog = dog
        _created(this)
    }

    override val firstFrameRecord: Record
        get() = record

    override fun prependFrameRecord(value: androidx.compose.frames.Record) {
        value.next = record
        record = value as Record
    }

    class Record : AbstractRecord() {
        var isAnimal: Boolean = false
        var cats: List<Cat> = emptyList()
        var dog: Dog? = null

        override fun assign(value: androidx.compose.frames.Record) {
            (value as Record).let {
                isAnimal = it.isAnimal
                cats = it.cats
                dog = it.dog
            }
        }

        override fun create(): androidx.compose.frames.Record = Record()
    }
}

class Cat(
    isRunning: Boolean,
    isWalking: Boolean
) : Framed {
    private var record = Record()

    private val readable
        get() = _readable(
            record,
            this
        ) as Record

    private val writable
        get() = _writable(
            record,
            this
        ) as Record

    var isRunning: Boolean
        get() = readable.isRunning
        set(value) {
            writable.isRunning = value
        }

    var isWalking: Boolean
        get() = readable.isWalking
        set(value) {
            writable.isWalking = value
        }

    init {
        record.isRunning = isRunning
        record.isWalking = isWalking
        _created(this)
    }

    override val firstFrameRecord: Record get() = record

    override fun prependFrameRecord(value: androidx.compose.frames.Record) {
        value.next = record
        record = value as Record
    }

    class Record : AbstractRecord() {
        var isRunning: Boolean = false
        var isWalking: Boolean = false

        override fun assign(value: androidx.compose.frames.Record) {
            (value as Record).let {
                isRunning = it.isRunning
                isWalking = it.isWalking
            }
        }

        override fun create(): androidx.compose.frames.Record = Record()
    }
}

class Dog(
    count: Int,
    type: Type
) : Framed {
    private var record = Record()

    private val readable
        get() = _readable(
            record,
            this
        ) as Record

    private val writable
        get() = _writable(
            record,
            this
        ) as Record

    var count: Int
        get() = readable.count
        set(value) {
            writable.count = value
        }

    var type: Type
        get() = readable.type
        set(value) {
            writable.type = value
        }

    init {
        record.count = count
        record.type = type
        _created(this)
    }

    override val firstFrameRecord: Record get() = record

    override fun prependFrameRecord(value: androidx.compose.frames.Record) {
        value.next = record
        record = value as Record
    }

    enum class Type {
        BULLDOG,
        DACHSHUND,
        SHIBA
    }

    class Record : AbstractRecord() {
        var count: Int = 0
        var type: Type = Type.BULLDOG

        override fun assign(value: androidx.compose.frames.Record) {
            (value as Record).let {
                count = it.count
                type = it.type
            }
        }

        override fun create(): androidx.compose.frames.Record = Record()
    }
}

なるほどー

第 1 水準以外の漢字を検出する

JIS 漢字コードの第 1 水準漢字以外を検出します。
Perl が一番楽そうだったので Perl 使いました。

何がしたいか

$ echo "perl" | hoge
false
$ echo "あいうえお。" | hoge
false
$ echo "漢字 | hoge
false
$ echo "弌腕。" | hoge # 第 2 水準漢字が入っている
true

第 1 水準漢字

第 1 水準漢字は亜から腕までの範囲を指します。
ただ、亜-腕のような正規表現でチェックするだけでは不十分です。

y0m0r.hateblo.jp

この記事に詳細は書いてあり、非常に参考になりました。
ということで範囲を調べる必要があります。

コード表を参照して、Shift JIS における文字コードの範囲を調べます。
亜だったら 889F, 腕は 9872 です。

一通り調べると、

  • 889F - 88FC
  • 8940 - 97FC
  • 9840 - 9872

の範囲であることが分かります。
これを正規表現に起こすとこんな感じ

\x88[\x9F-\xFC]|[\x89-\x97][\x40-\xFC]|\x98[\x40-\x72]

第 1 水準漢字以外の漢字

第 1 水準漢字の検出はできましたが、実際に欲しいものは違います。

漢字の検出は \p{Han} で可能です。
なので、\p{Han} で取得した漢字を先程の正規表現に当てて、引っかからないものが第 1 水準漢字以外としました。

ただ、\p{Han} だと句読点なども含まれます。\p{Script=Han} にすると句読点は除くことができますが、々などは入ってきます。
なので、先程の正規表現で記号も見ることにします。コード表によると記号は

  • 8140-81AC

あたりです (実際はもうちょっとあります、サボりました)

これを付け加えて

\x81[\x40-\xAC]|\x88[\x9F-\xFC]|[\x89-\x97][\x40-\xFC]|\x98[\x40-\x72]

これでふんわり記号 + 第 1 水準漢字です。ちゃんと見るなら 1 区から 8 区までの文字コードを追加すれば良いと思います。
この正規表現で引っかからない場合は第 2 水準漢字とか、そのへんです。

あと、第 1 水準漢字のところでは Shift JIS になっている必要があるので

encode("sjis", $hans);

こういう処理が必要です。

Repository はここ

github.com

おわり。

動的解析ツール Frida を Android に使う

Frida を使ってみたメモ

Frida

frida.re

自分の Script を Inject したり、値を取得したり、色々なことができる Toolkit です。
リバースエンジニアリングとかする時に使うみたいですね。
iOS / Android にも対応しており、今回は Android の話です。
ちなみに日本語の記事もいくつかあります。

もし試す場合は自分のアプリや許可されているものを使用して試してください。
ここからの手順やコマンド、コードによって何が起きても私は責任を取りません。

入れてみる

Rooted device のほうが簡単らしいのですが、持ってないので Without root で試します。
肝心の手順はここに全部書いており、写すことはしないのでこちらを参照してください。

koz.io

簡単に手順を書くと

1. apk を device から抜く (pm path とかしてから pull する)
2. apktool を用いて apk を decode する
3. 諸々いじった後、libfrida-gadget.so を lib/ に入れる
4. apktool で apk を build
5. jarsigner で署名 / 検証
6. zipalign で最適化

という感じです。

3 での諸々いじる工程もすべて上記の記事に書いてあります。

補足
  • AndroidManifest.xmlandroid:extractNativeLibs が false になっている場合は true にしないと Install 時に失敗します。
  • 最新の Frida は動かないという Issue が上がっており、実際に私も動かなかったので frida-gadget-12.7.26-android-*.so を使用しています。
  • frida-gadget を load するタイミングは onCreate のはじめとかでいいと思います

使ってみる

試したコードとアプリはここにあります。以降はこのデモ app を使用しています。

github.com

準備

Frida の install が終わっていない場合は

$ pip install frida-tools

さて、Frida は Python で書くのですが、Inject する Sctipt は JavaScript です。
ということでまずはベースとなる Python の Script が必要になります。

こちらをベースにして、

import sys
import frida


def on_message(message, data):
    print(message)


js = """
Java.perform(function() {
});
"""

process = frida.get_usb_device().attach('Gadget')

script = process.create_script(js)
script.on('message', on_message)
script.load()

sys.stdin.read()

こんな感じです。今回は Gadget を使っているので attach('Gadget') となります。
アプリが待ち受けている状態等で、

$ python example.py

を実行すると何も起きないはずです。
エラーが出る場合はおそらくどこか間違えています。

Detector の返す値を変更する

Repository を見てもらうと分かりますが jp.cordea.fridademo.Detector という Class があり、Detector#detect が false を返しています。
Detector#detect が true だと Button を click した際に toast が表示されます。ということで toast が表示されるようにします。

var detector = Java.use('jp.cordea.fridademo.Detector');
detector.detect.overload().implementation = function() {
    return true;
}

Button を click すると toast が表示されるはずです。

fab の click を上書きする

続いて fab の click を上書きします。まず fab を取得する必要があります。

var fabId = activity.findViewById(0x7f080069);
var fab = Java.cast(
    fabId.$handle,
    Java.use('com.google.android.material.floatingactionbutton.FloatingActionButton')
);

findViewById で fab を取得し、FloatingActionButton に cast しています。
なお、この ID はデモ app の場合は smali/jp/cordea/fridademo/R\$id.smali を検索することで取得できます。

Listener も必要です。

var listener = Java.use('android.view.View$OnClickListener');

そして Listener をセットします。

fab.setOnClickListener(Java.registerClass({
    name: 'jp.cordea.fridademo.OnClickListener',
    implements: [listener],
    methods: {
        onClick: function(v) {
        }
    }
}).$new());
TextView の count を上書きする

さて、先程 fab の click を上書きしたので今まで click 毎に +1 されていた TextView の count が動かなくなりました。
これを *2 するようにしてみましょう。

var textViewId = activity.findViewById(0x7f0800de);
var textView = Java.cast(
    textViewId.$handle,
    Java.use('android.widget.TextView')
);

そしてさきほどの onClick の中に *2 する実装を入れます

var count = 1;
fab.setOnClickListener(Java.registerClass({
    name: 'jp.cordea.fridademo.OnClickListener',
    implements: [listener],
    methods: {
        onClick: function(v) {
            count *= 2;
        }
    }
}).$new());

そして上書きしますが、java.lang.String としてセットする必要があります。
なので、以下のようにセットします。

var count = 1;
fab.setOnClickListener(Java.registerClass({
    name: 'jp.cordea.fridademo.OnClickListener',
    implements: [listener],
    methods: {
        onClick: function(v) {
            count *= 2;
            var string = Java.use('java.lang.String');
            textView.setText(string.$new(count.toString()));
        }
    }
}).$new());

これで fab を click すると +1 ではなく *2 で値が増えていく様子が見られると思います。
詳しくは Repository の example.py を見てください。

感想

使うためにはそれなりに知識が必要そうです。
Android の解析をするなら Android の知識、Frida の知識、Smali も軽く読める必要があります。
今回は難読化してないですが、難読化されている場合はそのあたりの知識も必要かもしれないですね。根気があればなんとかなりそうな気もしますが...

あと、ネイティブアプリエンジニアの観点から、これらにどう対処するかという話に興味がある場合
anti-frida や anti-reverse engineering で検索すると色々でてきますので見ると良いと思います。
中でも良かったものを 2 つ置いておきます

Podcast を Google Podcasts に登録する

outer-heaven.fm を先日 Google Podcasts に登録したので、その時のメモです。

podcasts.google.com

play.google.com

公式に手順がまとめられており、これを少し噛み砕いた内容です。

developers.google.com

1. Podcast の Feed URL を取得する

まずは Podcast の Feed URL が必要です。outer-heaven.fm では Hugo の Zen theme を使っており、こちらは Podcast に対応しているので RSS Feed を生成してくれます。

https://outerheavenproject.github.io/podcast/index.xml

2. Feed URL が Google に登録されるのを待つ

Google に登録されていない状態で Google Podcasts への登録を行おうとしても以下のようなエラーが表示されます。

f:id:CORDEA:20191231095432p:plain

outer-heaven.fm では URL を Search Console に登録し、
Feed URL をサイトマップに登録しておいて URL Inspection Tool で登録状況を確認していました。

これが正しいのかは分かりません...

3. Direct Link を生成する

Podcast Publisher Tools にアクセスし、フォームに Feed URL を入力して GENERATE を押します。

search.google.com

生成された URL を SNS にシェアしたりしましょう!
これで Google Podcasts app on Android の検索にも引っかかるようになります。