FCMを使ってAndroid端末へ通知を送るスクリプト

Firebase Cloud Messaging(FCM)を使ってAndroid端末へ通知を送るスクリプトを作りました(iOSは非対応です)
スクリプト内で送られているデータはcurlを使う場合を参照してください

作成したスクリプト

ソースコードhttps://github.com/sckm/fcm-send にあがっています

インストー

あらかじめgoがインストールされている必要があります

$ go get github.com/sckm/fcm-send

使い方

以下のように、はじめにjsonのdataキーで送りたい内容をファイルに書いておきます

{
    "message": "Hello World!",
    "title": "example title"
}

その後、以下のコマンドを実行することで通知が送られます

$ token='registration token'
$ server_key='your server key'
$ fcm-send -t $token -s $server_key -p data.json

curlを使う場合

作成したスクリプトは以下のcurlコマンドと同等のリクエストを送っています

token='registration token'
server_key='your server key'
msg='{"message":"Hello World!", "title": "example title"}'

curl \
    --header "Authorization: key=${server_key}" \
    --header Content-Type:"application/json" \
    https://fcm.googleapis.com/fcm/send \
    -d "{ 
        \"to\" : \"${token}\", 
        \"data\": $msg
    }"

FirebaseCrashReportingにmappingファイルをあげるスクリプトを作った

FirebaseCrashReportingにAndroidマッピングファイルをあげる際に相対パスが使えなかったり、google-services.jsonがapp直下にない時にAppIdなどを指定するのが面倒くさかったのでgolangスクリプトを作ってみました

ソースコードhttps://github.com/sckm/fcr-upload-mapping

動作環境

インストー

go get github.com/sckm/fcr-upload-mapping

使い方

cd [Androidプロジェクトのルートパス]
fcr-upload-mapping \
    -a [サービスアカウントキーファイルのパス] \
    -c [アプリバージョンコード] \
    -p [パッケージ名] \
    -m [マッピングファイルのパス] \
    -s [google-services.jsonのパス]

google-services.jsonがapp直下にある場合は省略可能です

その他

今はgradlewがあるディレクトリでしか動かないようになっていたり、引数の説明が足りないので修正したい
Golangに慣れていないのでコードが汚い・・・

RxJava2のSingle.zipのエラー処理ではまった話

問題

RxJavaでたまにonErrorReturnItemなどでエラー処理をしていてもたまにエラーがすり抜けてきてアプリが落ちてしまうことがあった
問題となっていそうな部分を簡単に表すと以下のようになっていた

Single.zip(
  Single
  Single
)
.onErrorReturnItem

対処方法

RxJavaのissuesを調べてみると同じ問題があった
Rx2: Multiple errors in zip'ed observables throw exception

ここに書いてある対処方法は、

  • RxJavaPlugins.setErrorHandler()でエラーハンドラーを設定する(issueのコメントではsetOnErrorになっているが名前が変更になった?)
  • zipの中のSingleでonErrorReturnonErrorResumeNextなどを呼び出す

いっけん良さそうに見えるので原因がわかりづらくてはまってしまった
実際にはどうしてそうなってしまうのかまだわかっていないのでRxJavaの実装を読んでいきたい

文字列リソースに複数の値を渡す

基本的な方法

複数の値を渡す際には%1$s%2$sなどを設定する。
1や2で何番目の引数を使うかを指定する

<string name="string_res1">1番目%1$s 2番目%2$s"</string>
getString(R.string.string_res1, "A", "B")

結果: 1番目A 2番目B

渡した値の一部を使わない場合

引数で複数の値を渡すが文字列リソースで使わない場合の挙動

<string name="string_res2">2番目のみ%2$s"</string>
getString(R.string.string_res2, "A", "B")

結果: 2番目のみB

特に問題なく文字列が取得できる

必要以上に値を渡した場合

1番目の引数のみ使う文字列リソースに2つの値を渡した場合

<string name="string_res3">1番目%1$s"</string>
getString(R.string.string_res3, "A", "B")

結果: 1番目A

実行時には問題なく文字列が取得できる
ただしlintでの警告がでる
values-ja/strings.xmlを作成して日本語の場合に2つの値を使うようにしても警告がでた

DataBindingでincludeしたレイアウトに値を渡す メモ

AndroidのDataBindingでincludeしたレイアウトに値を渡す時に少し悩んだのでメモ

参考: Data Binding Library

変数の渡し方法

includeされるファイルが以下のような時

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user1" type="com.example.User"/>
   </data>
   <merge>
       <include layout="@layout/name"
           bind:user="@{user1}"/>
       <include layout="@layout/contact"
           bind:user="@{user1}"/>
   </merge>
</layout>

includeする側のレイアウトではbind:hoge属性を使って変数を渡すことができる
hogeの部分にはlayoutで指定したファイル内の変数名を指定する(今回の場合はuser1)

<data>
    <variable name="user2" type="com.example.User"/>
</data>
<LinearLayout ...>
    <include layout="@layout/name"
        bind:user1="@{user2}"/
</LinearLayout>

参考にした公式サイトの情報ではbind:user="@{user}"となっていてすこしわかりづらいですね・・・

リソースファイルの値を渡す方法

もし、includeするlayoutにリソースファイルの値を渡したい時の例

例: Stringの値を受け取ることができるレイアウトファイル
"@{@string/app_name}"のように指定すると良い

<data>
    <variable name="title" type="String"/>
</data>
 <include
     layout="@layout/layout_text"
     bind:title="@{@string/app_name}"
     />

Backgroundに設定した画像を拡大させない方法

AndroidのViewのbackgroundに画像を設定した場合にViewのサイズに合わせて画像も拡大されます
通常は特に問題ないのですがどうしても画像を拡大させたくない場合がある時の対処方法を紹介します

今回は24x24dpの以下のような画像を使いました (AndroidStudioのImageAssetで作りました)

ic_weekend.png

画像サイズを維持するDrawableを作成する

以下のようなDrawableファイルを使うことで画像サイズを維持するDrawableを作成することが可能になります

<?xml version="1.0" encoding="utf-8"?>
<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center"
    android:src="@drawable/ic_weekend"/>

最低限必要なのはsrcのみで表示したい画像を設定します
gravityを設定することで表示位置を変更できます

実際の表示

以下のようなレイアウトを設定して表示を確認してみます
48x48dpと12x12dpの2通り

<View
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:background="@drawable/ic_weekend"
    />

<View
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:background="@drawable/no_scale_ic_weekend"
    />
    
<View
    android:layout_width="12dp"
    android:layout_height="12dp"
    android:background="@drawable/ic_weekend"
    />

<View
    android:layout_width="12dp"
    android:layout_height="12dp"
    android:background="@drawable/no_scale_ic_weekend"
    />

Screen Shot 2016-12-19 at 11.57.01 PM.png

2,4番目が今回作った、サイズを維持するdrawableを表示した場合です
1番目は拡大されて表示していますが2番目では期待通り24x24dpの表示になっています

おまけですが12x12dpの場合も見てみました
ただの画像を表示した場合はサイズに合わせて縮小されていますが、今回つくってDrawableでは一部分のみしか表示されていませんでした

まとめ

固定画像のサイズをViewのBackgroundに設定する際にはbitmap要素のDrawableファイルを作成すると良いようです

画像サイズより大きいViewのbackgroundに画像を表示する場合は問題ないですが、
小さいViewに設定する際には一部分のみしか表示されないので注意する必要があります

ProgressBar用にDrawableを作成メモ

基本的にはdrawの中で描画をしてあげるだけで良い
動きをつける場合にはgetLevel()で取得できる値を元に行うと簡単
getLevel()の値の範囲は[0, 10000]
サイズの変更はonBoundsChange(Rect)で受け取れる(drawの前に最初に呼ばれるが必ずそうなのかは要調査)

public class MyDrawable extends Drawable {
    private static final int INDICATOR_COUNT = 12;
    private static final int MAX_LEVEL = 10000;

    private RectF rectF = new RectF();
    private Paint p;

    private int centerX;
    private int centerY;
    private int width;
    private int height;


    public MyDrawable() {
        p = new Paint();
    }

    @Override public void draw(@NonNull Canvas canvas) {
        int duration = MAX_LEVEL / 4;

        int currentPos = ((getLevel() % duration) / (duration / INDICATOR_COUNT)) % INDICATOR_COUNT;

        for (int i = 0; i < INDICATOR_COUNT; i++) {
            // 先頭との差によって色を変える[0xaaaaa, 0xffffff]の範囲で
            int diff = ((currentPos + INDICATOR_COUNT) - i) % INDICATOR_COUNT;
            diff = Math.max(Math.min(diff, 0x4), 0x0);
            p.setColor((0xff000000 | (0x00111111 * (0xf - diff))));

            float r = 32 * width / 100f;
            canvas.drawRoundRect(rectF, r, r, p);
            canvas.rotate(360f / INDICATOR_COUNT, centerX, centerY);
        }
    }

    @Override public void setAlpha(int i) {

    }

    @Override public void setColorFilter(ColorFilter colorFilter) {

    }

    @Override protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        centerX = bounds.centerX();
        centerY = bounds.centerY();
        width = bounds.width();
        height = bounds.height();
        float scale = width / 100f;

        rectF.set(centerX + 24 * scale, centerY - 4 * scale, width - 2 * scale, centerY + 4 * scale);
    }

    @Override public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

今回作ったものは以下のような表示をする(背景は別で灰色にしています)

f:id:scache:20161216102154g:plain:w100

setAlphasetColorFilterについては調べられてないので調べる必要がある
また、getLevelを使う以外にもアニメーションを行える方法があるようなので他の方法も調べたい