JobDispatcherを使ってみる

# JobDispatcherとは

[firebase/firebase-jobdispatcher-android](https://github.com/firebase/firebase-jobdispatcher-android)

APILevel 21(Lollipop)で追加された`JobScheduler`の代替ライブラリです。完全に忘却の彼方でしたが、[Google IO 2016で紹介されていた](https://youtu.be/VC2Hlb22mZM?t=1623)みたいです。
`JobScheduler`の代替であることからわかるようにタスクスケジューラとして利用するものです。

`JobDispatcher`のバックエンドは`GooglePlayServices`の[`GCMNetworkManager`](https://developers.google.com/cloud-messaging/network-manager)に依存してます。一応実装が抽象化されていますが、現状は`GCMNetworkManager`のラッパーと考えて良さそうです。

本稿では`JobScheduler`の話はしませんが、**`JobDispatcher`は`JobScheduler`と同じインターフェースではない**ということに注意してください。

# そもそも何故生まれたか

Android 7.0では、`android.net.conn.CONNECTIVITY_CHANGE`を始めとするいくつかのブロードキャストについて、`BroadcastReceiver.register()`以外の方法でブロードキャストを受信できなくなります。(Manifestに書いたやつはダメということ)

`android.net.conn.CONNECTIVITY_CHANGE`はネットワーク状態の変更を通知してくれますが、この条件が雑すぎたことで送信頻度が高いため、システムがバックグラウンドで多くのアプリを起動する(BroadcastReceiverが起動される＝アプリが起動する)ことに繋がっていました。
多くのアプリは、「通信できるようになれば、xxする」という目的に使われているはずですが、「通信可能｜不可能状態への変化」「Wi-Fi｜モバイル回線切り替え」など、様々な条件で飛ぶようになっているので、やべえよやべえよ…ってことのようです。

# できること

現状、githubにあるドキュメントが雑なので、`GCMNetworkManager`のドキュメントも合わせて参考にしたほうがいいかもしれないです。
ただ、これらの機能がすべて`JobDispatcher`で提供されているのかは調べ切れてないです。

1. 1回または繰り返しのタスクスケジュールを提供
2. エラー時の再試行及びバックオフのサポート
3. 端末状態の条件付与 (ネットワーク状況、Wi-Fi only、充電状態限定)
4. ジョブの永続性 (端末再起動、アプリアップデートなどに対応)

特に1は、場合によっては`AlarmManager`いらねえんじゃねえかなとか思ったりしました。

# 利用

`dependencies`に`com.google.android.gms:play-services-gcm`を含んでいるか否かでわかれます。要するに、単品と`play-services-gcm`バンドル版があるということです。

単品

```
compile 'com.firebase:firebase-jobdispatcher:{latestVersion}'
```

バンドル版

```
compile 'com.firebase:firebase-jobdispatcher-with-gcm-dep:{latestVersion}'
```


# 実装

`JobDispatcher`を利用するために必要な実装は、主に2つに分かれています。

1. JobServiceの実装
2. Jobの構築

## JobServiceの実装

`JobService`は名前の通り、Androidの`Service`のことです。
`JobDispatcher`で、`JobService`と`SimpleJobService`というクラスが提供されているので、どちらかを継承しましょう。

```MyJobService.java
public class MyJobService extends SimpleJobService {

    @Override
    public int onRunJob(JobParameters job) {
        // any
    }
}
```

`JobService`と`SimpleJobService`の違いですが、コード見た感じだと`Service`と`IntentService`のようなものです。`SimpleJobService`は専用スレッドで`onRunJob()`メソッドが呼び出されます。

`JobService`を用意したら、忘れずにManifestに追加しましょう。[^1]

```AndroidManifest.xml
<application>

    …
    <service
        android:name=".MyJobService"
        android:exported="false">

        <intent-filter>
            <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
        </intent-filter>
    </service>

</application>
```


## Jobの構築

ジョブの実行条件は`Job`インスタンスを構築して行います。`Job`の構築には`FirebaseJobDispatcher`を使います。
`FirebaseJobDispatcher`の実装は抽象化されており、`Driver`インターフェースでDelegateできます。ただ、プリセットとして実装されているのは`GooglePlayDriver`しかないため、基本おまじないになります。

```
FirebaseJobDispatcher dispathcer = new FirebaseJobDispatcher(new GooglePlayDriver(context));
```

`FirebaseJobDispatcher`から、`Job`のBuilderを生成できます。

```
Job.Builder jobBuilder = dispatcher.newJobBuilder()
        .setService(MyJobService.class)
        .setTag("myJob")
        .setTrigger(Trigger.NOW)
        .setReplaceCurrent(true);
```

| メソッド | 用途 | 備考 |
|:---:|:---:|:---:|
| `setService(Class)` | 起動するServiceの指定 ||
| `setTag(String)` | ジョブを識別するIDを設定する | 100文字以内 |
| `setTrigger(JobTrigger)` | 着火猶予の指定 | 30秒未満は30秒になる(？) |
| `setLifetime(int)` | 永続性の指定 | 端末を再起動しても持続するかどうか |
| `setRecurring(boolean)` | 繰り返しの設定 | Trigger.NOWでは使えない |
| `setConstraints(int...)` | 端末状態条件の指定 |  |
| `setRetryStrategy(RetryStrategy)` | 再試行間隔のバックオフアルゴリズムの指定 |
| `setReplaceCurrent(boolean)` | 既存の同tagのジョブに対して上書きを行うかどうか |
| `setExtras(Bundle)` | タスクに渡す任意のBundle ||

Jobを構築したら、あとは登録するだけです。

```
dispatcher.schedule(jobBuilder.build());
```

ちなみにJobの取り消しはtagで行います。

```
dispatcher.cancel(tag);
```

# 定期的に繰り返す

Jobを繰り返すのは、`setRecurring(true)`とするだけですが、このとき`setTrigger()`には、`ExecutionWindowTrigger`を指定する必要があります。

```
FirebaseJobDispatcher dispathcer = new FirebaseJobDispatcher(new GooglePlayDriver(context));

Job.Builder jobBuilder = newJobDispatcher(this).newJobBuilder()
        .setTag("myPeriodicJob")
        .setService(MyJobService.class)
        .setRecurring(true)
        .setTrigger(Trigger.executionWindow(60, 120));

dispatcher.schedule(jobBuilder.build());
```

```
Trigger.executionWindow(60, 120)
```

この記述の場合、60～120秒程度のスパンで、ジョブを繰り返し実行するという指定になります。

# 即座に実行する

~~`Trigger.NOW`という指定を行うと、直近で1回だけ行うタスクを実行するジョブを作れます。しかし、これにも30秒制限があるようで、実際には**即座に実行されず30秒程あとに実行されます**。~~

~~つまり、**即時実行のジョブは作れない**ということになります。~~

`Trigger.NOW`の遅延を1秒に変更したよ！っていうPRがマージされていたので、早くて30秒後ではなくなったかもしれないです

[Bug fix and speed up Trigger.NOW by unEgor · Pull Request #174 · firebase/firebase-jobdispatcher-android](https://github.com/firebase/firebase-jobdispatcher-android/pull/174/commits/a35fc20be0a7090cfb3ee267c4a36a6594f48d4a)

# ジョブの保留

ネットワーク状態や端末状態を条件にした場合(`setConstraints()`)、条件を満たしていないと、時間になってもジョブが実行されず保留されます。保留されたジョブは条件を満たしたときに実行されます。


# 参考

[firebase/firebase-jobdispatcher-android](https://github.com/firebase/firebase-jobdispatcher-android)

[Implementing GCM Network Manager on Android | Google Developers](https://developers.google.com/cloud-messaging/network-manager)


[^1]: カスタム`IntentFilter`を書いているとデフォで`exported=true`になってセキュリティ的にあうあうあーってなるので注意してください