アプリ上でサーバの「メンテナンス中」をリアルタイムに伝える方法

# TL;DR

アプリは何も悪くないのにサーバ障害のせいでレビューががた落ち、というのは30分ぐらいの開発で防げるので実装しておこう

# アプリは悪くないのに評価が下がって辛いとき

どんなに完璧でバグがなく実装されたアプリでも、通信相手となるサーバ側で問題が起きてしまうとアプリは使えなくなってしまいます。

このとき、ずっと _読み込み中_ のままだったり _エラーが発生しました_ と表示され続けるだけになっていると、ユーザーにとっては __アプリそのものが__ 壊れているのと変わりません。「せっかくインストールしたのに、壊れてて使えない」わけなので、必然的にレビュー欄が荒れてしまいます。

一方、障害の発生時でも起動したアプリ中で __ごめん、いまなんとかしてるよ！__ ということが正しく伝えられれば、ユーザーもいきなり怒ったりはしません。

この記事では、

  * サービス提供側からのメッセージを
  * リアルタイムにユーザーに提示する
  * できる限り簡単な実装

について紹介します。

今回は Google が 2014 年に買収した [Firebase](https://www.firebase.com/) というリアルタイムバックエンドサービスを使います。以下ではAndoroidで説明してますが、[iOSでもCocoaPodsでサクッと導入して](https://www.firebase.com/docs/ios/quickstart.html)同じように作って共通化することができます。

# どんな感じ

「やばい、サーバ止まってる……」

そんな切羽詰まった状況下でも、Web上でステータスを更新することで、ユーザー側に状況をリアルタイムに伝えることができます。

![out2.gif](https://qiita-image-store.s3.amazonaws.com/0/12052/6d5506a6-ea09-ed65-70c7-20e5099fb678.gif)

 * 稼働状態 (active) が `false` になると、アプリに指定したメッセージの Snackbar が表示され、問題が発生していることをアプリのユーザーに伝えます。
 * 回復状況に応じて書き換えていくと、リアルタイムに反映されます

この実装本体は [諸々含めて ~~61行~~ 120 行](https://github.com/tnj/FirebaseMaintenanceMode/blob/master/app/src/main/java/sh/nothing/firebasemaintenancemode/MaintenanceMode.java) 程度で書かれており、 [Activity へこんな感じのコールバックイベント実装を追加する](https://github.com/tnj/FirebaseMaintenanceMode/commit/9c5f9f80ddc60d6373766d8d0ba98d782ad5a447#diff-245a42a6c4e333128c799b949e0c73c6R35) だけで実装できます。これだけでメンテ中にアプリ起動されたときや、通信が切れたり復活したりした場合も自動的に再接続が行わるなど、様々な状況下で正常に動作するようになっています。(Firebaseよくできてる)

リポジトリはこちら: https://github.com/tnj/FirebaseMaintenanceMode


# 作り方

まず Android Studio でいつもの `Blank Activity` プロジェクトを作ります。

## Android Studio でプロジェクトに Firebase を追加する

<kbd>Command</kbd> + <kbd>;</kbd> (Windows だと <kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd> ) で Project Structure を開き、 Cloud を開くと Firebase のチェックボックスがあるのでこれにチェックを入れて OK を押します。

![Screen Shot 2015-12-06 at 7.28.16 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/fe3af64c-7346-ae26-9604-3bbf03cc46c1.png)

以下の行が追加されます。

```groovy:app/build.gradle
compile 'com.firebase:firebase-client-android:2.3.1'
```

```xml:AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
```

あと、ライブラリのファイルが重複していてビルドがこけるので、手動で以下の設定を `app/build.gradle` へ追加します。

```groovy:app/build.gradle
android {
    // ...
    // 諸々の設定のあと
    // ...

    packagingOptions {
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/LICENSE-FIREBASE.txt'
        exclude 'META-INF/NOTICE'
    }
}
```

## Firebase のアカウントを作る

次に [Firebase のサイト](https://www.firebase.com/) の右上の _Sign Up_ からアカウントを作成します。 Google のアカウントを選択するだけです。

![Screen Shot 2015-12-06 at 7.39.22 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/af51d7e1-594d-eb4e-5721-19a72a7e2204.png)

サインアップできると _My First App_ という最初のアプリが既に作成されています。

## データを作る

_My First App_ の _Manage App_ を開くとデータの編集画面になります。

![Screen Shot 2015-12-06 at 10.47.43 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/c79ce392-d7b7-11e4-effd-48ff0d242846.png)


Firebase は基本的に JSON ライクな key-value のツリー型でデータを保存します。

今回は、 `status` という名前で、「サービスが生きているかどうか(`active`)」「死んでいるときに表示するメッセージ(`message`)」の2つをキーにデータを作ってみます。

親要素 (名前は自動生成) にカーソルを合わせると __+__ マークが現れるのでこれをクリックします。

![Screen Shot 2015-12-06 at 10.48.14 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/69584acb-010d-9898-6a3a-0cf39ee6ce76.png)

`name` に `status` と入力して、即値でなくて子要素を追加するため `value` は空のまま、その右の __+__ を押します。

![Screen Shot 2015-12-06 at 10.48.29 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/2d45828a-0325-7ae0-254d-f44083a4de05.png)

子要素の1つ目は name: `active`, value: `true` と入力して __Add__ を押します。

![Screen Shot 2015-12-06 at 10.58.28 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/20d1d64a-77ba-4299-454b-cb8cadac6cb7.png)

一旦ここまでのものがコミットされます。

![Screen Shot 2015-12-06 at 11.00.35 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/0c860029-51e1-fc53-07dc-9dfc73f5c388.png)

`status` に要素を追加するために、マウスカーソルを合わせると右に出てくる __+__ を押します。

![Screen Shot 2015-12-06 at 11.02.32 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/d2fc9965-6cd4-1500-cb28-6c3bd0f62206.png)

name: `message`, value: `""` と入力して __Add__ を押します。

![Screen Shot 2015-12-06 at 11.03.32 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/f5668af3-28f3-4c33-6a0a-8d303a28f955.png)

最終的にはこんな形になっているはずです。
 
![Screen Shot 2015-12-06 at 11.04.15 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/b77c0713-d657-fe7e-387b-3bd83dbe055b.png)

JSON で表現するとこんなシンプルな感じのデータです。

```json
{
  "status": {
    "active": true,
    "message": ""
  }
}
```

これでデータの準備ができました。

## セキュリティ＆ルールの設定

Firebase では読み書き権限やバリデーションのルールを _Security & Rules_ から JSON 形式で設定します。

![Screen Shot 2015-12-06 at 8.20.49 PM.png](https://qiita-image-store.s3.amazonaws.com/0/12052/4d45acb3-7cf7-69ed-b9e2-8239cc0f388c.png)

今回の status は読み取りは誰でもできてよく、書き込みは認証された人しか書けてはいけないので、以下のように設定します。

```json
{
  "rules": {
    "status": {
      ".read": true,
      ".write": "auth != null"
    }
  }
}
```

詳しくは[セキュリティのドキュメント](https://www.firebase.com/docs/security/quickstart.html)をご参照ください。

## アプリ側の実装

まず、 Firebase を `Application#onCreate()` で初期化します。

```java
Firebase.setAndroidContext(this);
```

次に、先ほど定義した `status` を受け取る POJO なオブジェクトを用意します。オプションですが、 `@IgnoreJsonProperties` アノテーションを付けておくと後々キーを追加したときにエラーにならずに済みます。

```java
@JsonIgnoreProperties(ignoreUnknown = true)
public class Status {
    private boolean active;
    private String message;

    public Status() {
    }

    public boolean isActive() {
        return active;
    }

    public String getMessage() {
        return message;
    }
}
```

次に、実際に受け取る部分を書きます。 Firebase ではデータの受け取りは常に非同期でコールバック経由で行われます。

```java
Firebase statusRef = new Firebase(FIREBASE_ROOT);
statusRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        Status status = dataSnapshot.getValue(Status.class);
        // ここで通知のブロードキャストを投げる
    }

    @Override
    public void onCancelled(FirebaseError firebaseError) {
        // エラー時の処理、今回は特に何もしない
    }
});
```

`FIREBASE_ROOT` には、 Firebase のデータ URL を書きます。これは先のデータ編集画面で、 `status` をクリックして開いた (status がルートになった) 状態のブラウザのアドレスバーに表示される URL そのものです。今回の場合は以下のアドレスになります。

```
https://fiery-inferno-8489.firebaseio.com/status
```

今回のアプリのメンテナンス情報は、アプリケーション全体で反映されるべき情報なので、この処理も `Application#onCreate()` のタイミングで呼ばれるようにします。 Activity などではブロードキャストイベントを受信して画面に反映するようにします。サンプルではブロードキャストにはイベント通知に実装が手軽な [otto](http://square.github.io/otto/) を使っていますが、 LocalBroadcastManager でも何でも問題ありません。

あとは受け取る側の Activity 側の実装を書きます。

```java
@Override
protected void onResume() {
    super.onResume();
    // イベントリスナを登録
    MaintenanceMode.bus().register(this);
}

@Override
protected void onPause() {
    super.onPause();
    // イベントリスナの登録解除
    MaintenanceMode.bus().unregister(this);
}

@Produce
public MaintenanceMode.Status getCurrentMaintenanceMode() {
    // 起動直後に呼び出される処理。現在知りうる最新のデータを返す。
    return MaintenanceMode.current();
}

@Subscribe
public void maintenanceModeChanged(MaintenanceMode.Status status) {
    // statusに変更があった場合に呼び出されるのでSnackbarを表示/非表示にする
    if (!status.isActive()) {
        bar = Snackbar.make(findViewById(R.id.coordinatorLayout), status.getMessage(), Snackbar.LENGTH_INDEFINITE);
        bar.show();
    } else if (bar != null && bar.isShown()) {
        bar.dismiss();
    }
}
```

できあがった[最終的な実装はこちら](https://github.com/tnj/FirebaseMaintenanceMode)。

# まとめ

ちょっとした仕組みですが、いざというときにあるととても役立ちます。機能追加の例としては、

  * URL を付けられるようにしてお知らせページへ誘導する
  * アプリケーションのバージョンやOSのバージョンにあわせて出し分けられる
  * FirebaseのREST APIを叩いてSlackから更新する仕組みを作る

などなどが考えられます。

ちなみに Firebase は同時 100 接続までは無料ですが、それを超える接続数を捌くためには月額 $25 からの利用料が掛かります。ただそれで接続数が無制限になるので、お仕事としてアプリを運用しているならサーバ代と考えれば全然元は取れると思います。もちろん普通にバックエンドとして使っても便利です。


## 追記

実機で試せるようにしてみました: [<img src="https://dply.me/c5mmvs/button/small" alt="Download to device">](https://dply.me/c5mmvs#install)

いま上のgifのように10秒に1回メンテになったり問題解消したりするbotを動かしてます。実際に手元で起動直後の動作を見たり、マナーモードにして実際に通信切ってみたりしたときの動作が試せます。