パルカワ2

最近はFlutterをやっています

ノンデザイナーズ・デザインブックを読んだ

ノンデザイナーズ・デザインブック [第4版]

ノンデザイナーズ・デザインブック [第4版]

なんとなく認識しているようなことを言語化されていてよかった。

Android Studio 3.5にあげた

CIではテストが通るんだけど、手元では通らなくなってしまったので困っていた。

2019-09-25 15:29:48:978 (KOIN)::[e] Error while resolving instance for class 'com.google.gson.Gson' - error: org.koin.error.BeanInstanceCreationException: Can't create definition for 'Single [name='Gson',class='com.google.gson.Gson']' due to error :
2019-09-25 15:29:48:978 (KOIN)::[i] [Close] Closing Koin context
		com.google.protobuf.Internal and com.google.protobuf.Internal$ProtobufList disagree on InnerClasses attribute
		java.lang.Class.getDeclaringClass0(Native Method)
		java.lang.Class.getDeclaringClass(Class.java:1235) 

org.koin.error.BeanInstanceCreationException: Can't create definition for 'Single [name='Gson',class='com.google.gson.Gson']' due to error :
		com.google.protobuf.Internal and com.google.protobuf.Internal$ProtobufList disagree on InnerClasses attribute
		java.lang.Class.getDeclaringClass0(Native Method)
		java.lang.Class.getDeclaringClass(Class.java:1235)



onPageFinished が実行されないときがある

Android の WebViewClient の onPageFinished が実行されないときがあるので困った。 onPageFinished は 2回実行されることもあるので、なかなかの困ったちゃんである。

onPageFinishedが実行されないときも onPageStarted は実行されるので、onPageStartedのときにすでに document.readyStateが loading 以外なら実行. loading なら監視してStateが変わったら実行という形にした。 onPageFinished が実行されないときは、 readyState が complete になってないようなので、WebViewが悪いというよりサイトが原因なんだろうと思う(しかし、すべてのサイトに修正を加える力は我々にはない………)

Moneytreeで未対応のTHEOを勝手に対応する

Moneytreeは現金とかタンス預金用に手動入力のみの「その他の口座」項目がある。
それは手動でしか入力できないんだけど、チョットプログラム書けば未対応のTHEOも自動で入力できるじゃんと思ったのでやってみた。puppeteer便利すぎ

流れ

  • THEOにログイン
  • THEOで表示されてる額を取得
  • Moneytreeにログイン
  • つくっておいた口座に額を追記

これをなんらかの方法で定期実行すればよい

実装

こういう感じ

import * as puppeteer from 'puppeteer';
import {Page} from "puppeteer";

class Theo {
    public static get username(): string { return ""; }
    public static get password(): string { return ""; }
}

class Moneytree {
    public static get username(): string { return ""; }
    public static get password(): string { return ""; }
}


class LoadTheoMarketPrice {
    async execute(page: Page): Promise<string> {
        await page.goto('https://app.theo.blue/account/login');

        await page.type("input[name=email]", Theo.username);
        await page.type("input[name=password]", Theo.password);
        await page.tap("#app > section > section > form > div > button");
        await page.waitForNavigation();

        return await page.$eval("span.number-value", (span) => span.textContent.trim());
    }
}

class SaveTheoMarketPrice {
    async execute(page: Page, price: string) {
        await page.goto('https://app.getmoneytree.com/login');

        await page.type("input[name=email]", Moneytree.username);
        await page.type("input[name=password]", Moneytree.password);
        await page.tap("#app > div > div > div.onboardingPageBody > form > div > button");
        await page.waitForNavigation();

        // 口座残高を表示
        await page.goto("https://app.getmoneytree.com/app/vault");
        await page.waitFor(3000);

        // THEOの項目を表示/人によって違うよ
        await page.tap("div > ul > li:nth-child(4) > div.row.institution-header > a");
        await page.tap("#mt-webapp > section > mt-webapp-layout > div > div.content.ng-scope > mt-accounts > div > mt-accounts-personal > div > mt-link-vault > div > mt-two-column-layout > div > div.column.left.sidebar > div:nth-child(2) > left-column-body > mt-credentials > div > ul > li:nth-child(4) > div:nth-child(2) > ul > li.ng-scope > div > mt-credential > div > mt-credential-template > div > div.credential-accounts > div.credential-body > mt-list-secondary > div > ul > li:nth-child(2)");
        await page.waitFor(3000);

        // 編集ダイアログを表示
        await page.tap("#mt-webapp > section > mt-webapp-layout > div > div.content.ng-scope > mt-accounts > div > mt-accounts-personal > div > mt-link-vault > div > mt-two-column-layout > div > div.column.center.content > mt-awesome-layout > div > div > div:nth-child(1) > awesome-body > div > right-column-body > div:nth-child(2) > div.ng-scope > mt-manual-account-balances > div > div.current-account-balance-header > span:nth-child(2) > a");

        // 保存されてる内容を消す
        await page.$eval("#mt-webapp > section > mt-webapp-layout > div > div.content.ng-scope > mt-accounts > div > mt-accounts-personal > div > mt-link-vault > div > mt-two-column-layout > div > div.column.center.content > mt-awesome-layout > div > div > div:nth-child(1) > awesome-body > div > right-column-body > div:nth-child(2) > div.ng-scope > mt-manual-account-balances > div > div.current-account-balance-header > span:nth-child(2) > mt-manual-account-balances-action > div > div:nth-child(5) > mt-amount-input > div > div.pull-right > input",
            value => (value as HTMLInputElement).value = "");

        // priceを入力
        await page.type("#mt-webapp > section > mt-webapp-layout > div > div.content.ng-scope > mt-accounts > div > mt-accounts-personal > div > mt-link-vault > div > mt-two-column-layout > div > div.column.center.content > mt-awesome-layout > div > div > div:nth-child(1) > awesome-body > div > right-column-body > div:nth-child(2) > div.ng-scope > mt-manual-account-balances > div > div.current-account-balance-header > span:nth-child(2) > mt-manual-account-balances-action > div > div:nth-child(5) > mt-amount-input > div > div.pull-right > input",
            price);

        // + にする
        await page.tap("#mt-webapp > section > mt-webapp-layout > div > div.content.ng-scope > mt-accounts > div > mt-accounts-personal > div > mt-link-vault > div > mt-two-column-layout > div > div.column.center.content > mt-awesome-layout > div > div > div:nth-child(1) > awesome-body > div > right-column-body > div:nth-child(2) > div.ng-scope > mt-manual-account-balances > div > div.current-account-balance-header > span:nth-child(2) > mt-manual-account-balances-action > div > div:nth-child(5) > mt-amount-input > div > div.pull-left.ng-scope > div > i");

        // 更新する
        await page.tap("#mt-webapp > section > mt-webapp-layout > div > div.content.ng-scope > mt-accounts > div > mt-accounts-personal > div > mt-link-vault > div > mt-two-column-layout > div > div.column.center.content > mt-awesome-layout > div > div > div:nth-child(1) > awesome-body > div > right-column-body > div:nth-child(2) > div.ng-scope > mt-manual-account-balances > div > div.current-account-balance-header > span:nth-child(2) > mt-manual-account-balances-action > div > div:nth-child(7) > a");
        await page.waitFor(3000);
    }
}

exports.saveTheoMarketPrice = () => {
    (async () => {
        const browser = await puppeteer.launch();
        const theo = await browser.newPage();
        let price = await new LoadTheoMarketPrice().execute(theo);
        price = price.replace(",", "");

        const moneytree = await browser.newPage();
        await new SaveTheoMarketPrice().execute(moneytree, price);

        await browser.close();
    })();
};

一応書いておくとこのコードによってなにか起きても責任は取りません。

プール行ってる

もともとはジムに行ってたんだけど、時間効率を考えると水泳だな…と思い始めて区のスポーツセンターに通い始めた。

最初は、仕事が終わってダラダラして21時頃に勢いで下の水着(正確には水陸両用パンツ)だけ持っていったら帽子が必要ですってプールサイドで言われて「入り口の注意事項に書いてなかったやん」とは思ったんだけど、どうせならもろもろ揃えるかと思って、2時間分の入場料払って5分でそのまま帰った(スポーツセンターの売店で買えるらしかったけど)

次の日、大きめのスポーツ用品店で度付きのゴーグルとしっかりした水着と帽子を買って二回目。 夜は人が少なくて目立つのが嫌だったので、人間がたくさんいそうな時間帯を狙った。16時くらい。 レーンへの入り方みたいなのもわかってなかったので、最初はプールを眺めて雰囲気を理解した。このとき、変質者と思われないか不安だった。

中学生くらいまではプール教室?に行ってたので泳ぐのは出来たんですけど、まともに泳ぐのは中学生ぶりだし今は泳げるか不安だったのでまずは水中で歩いた。 水の中での歩き方にも色々あると思うんだけど、何も知らないのでそれも雰囲気でやった。

歩くのは余裕すぎるな〜という感じだったので、初心者レーン25mにシュッと入って泳いだら案外泳げて楽しい。 こりゃあいいと思って、25mを何度か往復していたらまあ疲れる。疲れたら歩いて休憩。回復したらまた泳ぐを繰り返した。

そしたら、足と胸あたりの筋肉が悲鳴を上げ始めたのでジャグジーに入って帰宅した。筋肉痛になりたくないので筋膜リリースして終わり。 想像してたより楽しかったので続ける。ジムは解約する予定

アマニタ・パンセリナ を読んだ

アマニタ・パンセリナ (集英社文庫)

アマニタ・パンセリナ (集英社文庫)

中島らも自身がドラッグに関する情報について、咳止めシロップの項目でこう書いてる。

問題は情報にはなくほとんどいつも中毒者の側にある。
つまり、なるべくして中毒になるような、そういう器質の一群があるのだ。

情報として重要なのは、たとえばこの文章のような「正確な」情報だろう。
こうすればこうなりますよ、という臨床例に近いような情報だ。
かくすより「知らせる」ことの方が大事なのである。

grpc-java/protobuf-lite では JsonFormat がない

Unable to find JsonFormat class in Android · Issue #276 · google/protobuf-gradle-plugin · GitHub

Androidprotobuf-liteを利用したまま、JSONをオブジェクトにする必要があったので、Gsonで頑張ることにした。InternalなAPIを呼ぶ必要があり困っている。ちなみに grpc/grpc-swiftは頑張る必要もなくシュッと出来ます。

  • protoで定義されたフィールドは、生成されたクラスのメンバー変数では最後に_ がつく
    • 例えば、 name というフィールドを持ってるとすると生成されたクラスのメンバー変数では name_ になる
  • protoで定義されたフィールドでリピートするものは、ただのListではなくInternal.ProtobufList<T>で表現されている
    • TypeAdapter がないとエラーになる

以上をふまえて、以下のようなコードを書いた。

package com.google.protobuf

open class ProtobufListTypeAdapter<T>(val parser: (JsonElement) -> Collection<T>): TypeAdapter<Internal.ProtobufList<T>>() {
    override fun write(out: JsonWriter?, value: Internal.ProtobufList<T>?) {
        // 利用していないので実装していない
    }

     override fun read(jsonReader: JsonReader): Internal.ProtobufList<T> {
        val list = ProtobufArrayList<T>()
        list.addAll(parser(JsonParser().parse(jsonReader)))
        return list
    }
}

// 型ごとに作ることになるが、リフレクションで頑張ればどうにかできそう(試していない)
 class DeliveryTimeListTypeAdapter: ProtobufListTypeAdapter<DeliveryTimeOuterClass.DeliveryTime>({
    it.asJsonArray.map { element ->
        DeliveryTimeOuterClass.DeliveryTime.newBuilder()
            .build()
    }
})

ProtobufArrayListが、com.google.protobuf内でしか利用できないので、package com.google.protobufにしている。

class ProtoExclusionStrategy: ExclusionStrategy {
    override fun shouldSkipClass(clazz: Class<*>?): Boolean {
        return false
    }

    override fun shouldSkipField(f: FieldAttributes?): Boolean {
        if (f == null) {
            return true
        }
        // proto で定義されたフィールド以外は、JSON化しない
        return !isProtoField(f.name)
    }

    fun isProtoField(name: String): Boolean {
        // filedに _ があると proto で定義されたフィールドとする
        return name.contains("_")
    }
}

val gson = GsonBuilder()
    .setFieldNamingStrategy { it.name.replace("_", "") }
    .setExclusionStrategies(ProtoExclusionStrategy())
    .registerTypeAdapter(
        object : TypeToken<Internal.ProtobufList<DeliveryTimeOuterClass.DeliveryTime>>(){}.type,
        DeliveryTimeListTypeAdapter()
    )
    .create()

gson.fromJson(...)

もっといい方法があったら知りたい。