Firebase Cloud Messaging(FCM)のPUSH通知をトリガーに、スリープ状態から復帰する

Androidの場合、スリープ状態でPUSH通知を受けると、音だけ再生されて画面は真っ暗。

それだと気づけないので、LINEとかのPUSH通知とかは、PowerManagerで電源をONにした後にPUSH通知を作って表示したりしてます。

FCMで普通にやるとPUSH通知の受信から制御できないので、

どうやったらできるようになるのか、FCMの通知の仕組みから見ていきます。

FCMには通知が2種類存在します。

参照:FCM メッセージについて  |  Firebase

通知 通知の表示 通知の受信を制御 通知のタップを制御
通知メッセージ Auto X
データメッセージ Manual

凄くざっくりした感じですが、簡単に通知を表示するなら「通知メッセージ」を使って、

通知の受信をハンドリングしたり、通知の表示をカスタマイズしたい場合は「データメッセージ」を使います。

今回は、受信をハンドリングして電源をONにしたいので、「通知メッセージ」ではなく「データメッセージ」を利用します。

2種類の通知はAPIを叩く際に渡すJSONの構造によって決まります。

通知メッセージの場合は、 notification に通知内容を設定します。(dataも設定可能です)

{
  "to" : "axd2Wedbd2Wed2WBed9ebVeewdbd2WedbWq1",
  "notification" : {
    "body" : "hello",
    "title" : "greeting"
  },
  "data": {
    sound: 'ping'
  }
}

データメッセージの場合は data のみに通知内容を設定します。

{
  "to" : "axd2Wedbd2Wed2WBed9ebVeewdbd2WedbWq1",
  "data" : {
    "body" : "hello",
    "title" : "greeting"
  }
}

今回は、データメッセージを使いたいので、dataのみに値を設定してFCMのAPIを叩いてPUSH通知を送信します。

ここまでで、サーバー側の仕事は完了です。

後は、アプリ側でデータメッセージを受けて処理するだけです。

仕事上、最近、ReactNativeを使う機会が多いので、RNのコード例です。

import { AppRegistry } from "react-native";
import Wakeful from "react-native-wakeful";
import App from "./App";

AppRegistry.registerComponent("app", () => App);

AppRegistry.registerHeadlessTask(
  "RNFirebaseBackgroundMessage",
  () => async message => {
    Wakeful.acquire(); // 電源をON
    return Promise.resolve();
  }
);

PUSH通知を作る所までコード例を書いてないですが、RNでPUSH通知を作るか、

AppRegistry.registerHeadlessTask せずに、Android側でRNFirebaseMessagingServiceを継承して、

onMessageReceivedでPowerManagerで電源入れて、PUSH通知つくることまでやっちゃっても良い気がします。

React: Componentをreconstructする。

Componentをconstructorから再実行させたい場合、keyを変えてやれば良い。

(ReactNative)

<FlatList
  key={tournament.roundName}
...

FlatListの initialScrollIndex は、リストアイテムの表示後、1回しかスクロールされない。

リストアイテムを更新した時も再度、スクロール位置を決めたい場合は、renderし直すのではなく、Component自体を作り直す必要があります。

FlatListに関しては、 scrollToIndex っていうメソッドが生えてますが、実行タイミングによっては、 スクロールが中途半端になってしまう事と、 initialScrollIndex の方が無駄なリストアイテムの描画が減る。

「やさしいLispの作り方」を読んで言語の作り方を学ぶ

「やさしいLispの作り方」はC言語で簡単なLispの処理系を実装して、REPLで動かすまでが書いてあります。

低レイヤーの知識がまったくない僕にとっては学びが多かった。

やさしいLispの作り方: C言語で作るミニミニLisp処理系

やさしいLispの作り方: C言語で作るミニミニLisp処理系

 REPLって何?

インタラクティブにコードを実行できる環境ぐらいの認識でしかなかった。

まあ、間違いじゃないんだけど。

「Read-eval-print-loop」って、それはそうなんですが、本当に、「標準出力から読み取って、評価して結果を標準出力に返して、それをループする」だけのプログラムだって事に、実際に書いてみてより正確に理解できた気がします。

Lispだから良い

(+ 1 2)

Lispの構文(S式)はかなり単純。

S式じゃなければ、構文解析するだけでもかなり大変です。

構文解析よりも、標準出力から読み取ったコードをどうやってメモリに乗せて評価してくのかを見たいので、S式が最適だと思いました。

Heap(ヒープ)とStack(スタック)

メモリ管理の方法として、ヒープとスタックと呼ばれる領域がある事は、知ってるぐらいの知識でした。

本書では、構文解析した結果や、ユーザーが定義した変数、関数定義などはヒープ領域に格納して、GCに必要な変数はスタック領域に格納していました。

一般的にユーザーがアクセスできる領域がヒープで、スタックはアクセスできない領域だと理解していいのかな?

ヒープとスタック | 学校では教えてくれないこと | [技術コラム集]組込みの門 | ユークエスト株式会社

そしてただのC言語の配列として表現されてるだけなので難しくないです。

標準出力から読み取ってメモリに乗せる(Read)

評価できるように、標準出力の内容をHeap領域に乗せていきます。

メモリに乗せる時は、文字列をプログラムが認識できる最小単位に分解(トークナイズ)します。

f:id:hikoukii:20180422111719p:plain

ただ、ヒープ領域に適当に乗せていくと、順序や関連がわからなくなってしまうので、

数珠繋ぎに前後のアドレス(配列の添字)を記憶できる構造体に値を入れてヒープ領域(配列)に詰めて行きます。

まず、シンボルや数値を左から順番に詰めて行き、再帰的に鎖を繋いで行く感じです。(CARが前でCDRが後の繋がり)

f:id:hikoukii:20180422185602p:plain

(図:ヒープ領域をDUMPしたもの)

S式を評価する(Eval)

Readすることで、ヒープ領域に評価できるS式がメモリに乗ったので、後は評価するだけです。

eval関数には、最後にreadしたアドレスを与えることで、鎖の初めから評価していきます。

鎖の先頭がシンボル(+)なので、C言語で書いた可変長引数を受け取って足し算する関数 int f_plus(int arglist) を呼び出します。 (シンボルと関数の紐付けはREPL起動時にヒープ領域に初期化しています。)

あとは、CDRにアクセスして残りの引数を int f_plus(int arglist) に渡して計算結果をヒープ領域に追加します。

0000148 FRE NUM    0000000 0000000 0000024 (null)

標準出力に表示する(Print)

PrintはReadの逆で、ヒープ領域にある結果を文字列に復元します。

Evalで構築された鎖の先頭アドレスを受け取り、終端まで文字列化していくだけです。

本記事の例では、 0000148 が Print に渡ってきて、TAGがNUMになってるので、24を取り出して標準出力に出力します。

再度受け付ける状態にする(Loop)

標準出力から再度、S式を受け取れる状態にしてREPLとしての振る舞いは完了です!

本書の通り実装すると、変数(setq)もネストしたS式も扱えるようになります。

長くなってしまうので、その部分の解説は避けますが、基本的な流れは本記事で紹介した流れに沿って、分岐していきます。

後、GCも mark-and-sweep で実装されてるので、別記事で解説できればやろうと思います。

実際に実装してみて詳細の動作を確認したい方は、¥500で買えるので是非!

grpc-gatewayでCookieをmetadataに設定しない

grpc-gatewayはデフォルトで、一般的なHeaderの値を、Metadataに入れてリクエストを中継します。

ただ、Cookieの値は使わない場合もあるし、 ユーザーに見えるkeyよりも、もっと意味持たせたkeyをMetadataに入れたいとかある。

そんな時は、mux作るときに、ServeMuxOptionを渡してあげれば良いです。

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux(
        runtime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
            if key == "Cookie" { // CookieはMetadataに設定しない。
                return "", false
            }

            return runtime.DefaultHeaderMatcher(key)
        }),
    )
    opts := []grpc.DialOption{grpc.WithInsecure()}

    // ...

    return http.ListenAndServe(":8080", mux)
}

grpc-gatewayにAPIを実装する

これはかなりおすすめしないですが、とりあえず検証ということでやってみました。

helthcheckぐらいならいいかも?

まずは、登録するハンドラーを用意。

package echo

import (
    "io"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "google.golang.org/grpc/grpclog"
)

func RegisterEchoHandlerFromEndpoint(mux *runtime.ServeMux) (err error) {
    // endpoint: /echo
    mux.Handle("GET", pattern_echo, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
        // レスポンスを書き込む
        if _, err = w.Write([]byte("hello.")); err != nil {
            grpclog.Printf("Failed to write response: %v", err)
        }
    })

    return nil
}

var (
    pattern_echo = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"echo"}, ""))
)

あとは登録するだけで動きます。

   ...
 
    if err := gw_echo.RegisterEchoHandlerFromEndpoint(mux); err != nil {
        return err
    }

    return http.ListenAndServe(":8080", mux)

リクエストしてみる。

→ curl -i -XGET http://localhost:8080/echo
HTTP/1.1 200 OK
Date: Thu, 18 Jan 2018 09:40:38 GMT
Content-Length: 6
Content-Type: text/plain; charset=utf-8

hello.%

1つのgrpc-gatewayに複数のAPI(gRPC)サーバーを繋ぐ

1つのgrpc-gatewayに複数サービス繋いで、認証は共通化するみたいなの作りたかったので調べてみました。

実装方法は簡単で、RegisterXXXXHandlerFromEndpoint に渡すendpointを変えればいいだけでした。  

package main

import (
    "flag"
    "net/http"

    "github.com/golang/glog"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"

    gw_user "protocol/user"
    gw_cart"protocol/cart"
)

var (
    userEndpoint = flag.String("user_endpoint", "localhost:50051", "endpoint of YourService")
    cartEndpoint = flag.String("cart_endpoint", "localhost:50052", "endpoint of YourService")
)

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}

    // localhost:50051と通信する
    if err := gw_user.RegisterUserHandlerFromEndpoint(ctx, mux, *userEndpoint, opts); err != nil {
        return err
    }

    // localhost:50052と通信する
    if err := gw_cart.RegisterCartHandlerFromEndpoint(ctx, mux, *cartEndpoint, opts); err != nil {
        return err
    }

    return http.ListenAndServe(":8080", mux)
}

func main() {
    flag.Parse()
    defer glog.Flush()

    if err := run(); err != nil {
        glog.Fatal(err)
    }
}

grpc-gatewayでネストされたリクエストパラメタを指定する

gRPCで、ValueObject的にサービスのリクエストメッセージを定義することはよくあるのかなーと思います。

ただ、grpc-gatewayを使う際に、型がネストするので、パラメタの指定がわかりにくかった。。

まとめられてるページもなさそうだったので、ここでまとめてみます。

クエリパラメタ

.区切りでプリミティブな型まで指定します。

こんな感じのリクエストメッセージがあるとすると、

service User {
  rpc FetchByName (UserFetchByNameRequest) returns (UserFetchByNameResponse) {
    option (google.api.http) = {
      get: "/users"
    };
  };
}

message UserFetchByNameRequest {
  User.Name name = 1;
}

message User {
  ...

  message Name {
    string value = 1;
  }
}

?name.value="hikouki"でクエリパラメタを指定できます。

curl -i -XGET http://localhost:8080/users?name.value="hikouki"

パスパラメタ

クエリパラメタと同じく、.区切りでプリミティブな型まで指定します。

こんな感じのリクエストメッセージがあるとすると、

service User {
  rpc Fetch (UserFetchRequest) returns (UserFetchResponse) {
    option (google.api.http) = {
      get: "/users/{id.value}"
    };
  };
}

message UserFetchRequest {
  User.ID id = 1;
}

message User {
  ...

  message ID {
    string value = 1;
  }
}

/users/1でパスパラメタを指定できます。

curl -i -XGET http://localhost:8080/users/1

リクエストボディ

オブジェクトでネストして指定します。

こんな感じのリクエストメッセージがあるとすると、

service User {
  rpc Create (UserCreateRequest) returns (UserCreateResponse) {
    option (google.api.http) = {
      post: "/users"
      body: "*"
    };
  };
}

message UserCreateRequest {
  User.Name name = 1;
}

message User {
  ...

  message Name {
    string value = 1;
  }
}

JSONで、'{"name": {"value": "hikouki"}}'をリクエストボディに指定します。

curl -i -XPOST http://localhost:8080/users -d '{"name": {"value": "hikouki"}}'