Jobschedulerの基本

# 検証環境
この記事の内容は、以下の環境で検証した。
* Java:open jdk 1.8.0_152
* Kotlin 1.2.10
* Android Studio 3.0.2
* CompileSdkVersion:26

# はじめに
今思えば、Oreoでサービスに関する変更があり、その布石だったJobSchedulerがAndroid 5.0(Lollipop：API Level21)で追加されいたんですね。この記事では、そんなJobSchedulerの最小限の実装方法を説明して行きます。

# 全体像
JobSchedulerが一体どういうものなのかを図にしてみました。

![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/99960/cea2382c-244c-3b23-2d14-eb89aacb2159.png)


図からわかるように、Serviceと違い条件が合致した時に実行されます。
また、ある一定期間でJobServiceなるものを繰り返し実行が可能です。

# 必要なクラス群
JobSchedulerを使用する上で必要となるクラス群とその役割は、以下のとおりです。

|クラス名|役割|
|:---|:----|
|android.app.job.JobScheduler|ジョブを管理します。登録されたジョブはIDで識別され、キャンセルなども行えます。|
|android.app.job.JobService|定期的に実行される処理を定義するクラス。|
| android.app.job.JobInfo |Jobが動作する条件やJobServiceを指定したもの。ジョブそのものの情報が格納されています。|
|android.app.job.JobInfo.Builder|JobInfoを生成するためのビルダークラス。|

# 実装方法
## 全体像
まずは、サンプルで作成したアプリのコードを全て掲載します。
その後、詳細な説明をします。

### AndroidManifest.xml
JobServiceもServiceを継承したものなので、AndroidManifest.xmlに登録する必要があります。
また、端末が再起動した時に、Jobを継続するために「uses-permissions」も追加しています。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.co.casareal.jobscheduler">

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".CasarealJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"></service>
    </application>

</manifest>
```

### レイアウトリソースファイル
JobSchedulerの登録ボタンとキャンセルボタンを保持した画面のレイアウトファイルです。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="jp.co.casareal.jobscheduler.MainActivity">

    <Button
        android:id="@+id/buttonJobStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Job開始！"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="@+id/buttonJobCancel"
        />

    <Button
        android:id="@+id/buttonJobCancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="52dp"
        android:text="Jobキャンセル！"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonJobStart"
        app:layout_constraintVertical_bias="0.046" />

</android.support.constraint.ConstraintLayout>
```

### サービス
Jobで実行する処理を定義しています。

```kotlin:CasarealJobService.kt
package jp.co.casareal.jobscheduler

import android.app.Notification
import android.app.NotificationManager
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Context
import android.util.Log

class CasarealJobService : JobService() {
    override fun onStopJob(params: JobParameters?): Boolean {

        jobFinished(params, false)
        return false
    }

    override fun onStartJob(params: JobParameters?): Boolean {

        Thread(
                Runnable {
                    val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
                    val notification = Notification.Builder(applicationContext)
                            .apply {
                                setContentTitle("JobSchedulerの通知")
                                setContentText("スケジュールで呼び出された処理で通知")
                                setSmallIcon(R.drawable.ic_launcher_background)
                            }.build()

                    manager.notify(1, notification)
                    Log.i(javaClass.name, "スケジュールしたジョブで呼び出された処理")

                    jobFinished(params,false)
                }
        ).start()

        return true
    }

}
```

### アクティビティ
JobInfoを生成して、JobSchedulerに登録しています。

```kotlin:MainActivity.kt
package jp.co.casareal.jobscheduler

import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.content.ComponentName
import android.content.Context
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    val jobId = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }

    override fun onResume() {
        super.onResume()
        val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler

        buttonJobStart.setOnClickListener {
            val componentName = ComponentName(this,
                    CasarealJobService::class.java)

            val jobInfo = JobInfo.Builder(jobId, componentName)
                    .apply {
                        setBackoffCriteria(10000, JobInfo.BACKOFF_POLICY_LINEAR);
                        setPersisted(false)
                        setPeriodic(0)
                        setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE)
                        setRequiresCharging(false)
                    }.build()
            scheduler.schedule(jobInfo)
        }

        buttonJobCancel.setOnClickListener {
            scheduler.cancel(jobId);

            // 下記の方法だとアプリで登録したすべてのJobがキャンセルされる
            scheduler.cancelAll()
        }
    }
}
```

## 詳細な説明

### AndroidManifest.xml
AndroidManifestでJobSchedulerが影響している箇所の詳細について説明します。

```xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```
端末が再起動しても、Jobを実行するには、「android.permission.RECEIVE_BOOT_COMPLETED」のパーミッションを得る必要があります。


```xml
<service
    android:name=".CasarealJobService"
    android:permission="android.permission.BIND_JOB_SERVICE"></service>
```
JobServiceはServiceを継承しているため、実行するにはAndroidManifestに登録する必要があります。そして、android:permission属性に、「android.permission.BIND_JOB_SERVICE」を指定します。

### サービス
サービスはこれまでにもあるように、android.app.job.JobServiceクラスを継承します。

```kotlin
class CasarealJobService : JobService() {}
```

JobServiceクラスを継承したクラスは、下記の2つのメソッドをオーバーライドしなければなりません。それぞれのメソッドの役割と戻りの違いについては表を確認してください。また、オーバーライドはしませんが、重要な「jobFinishedメソッド」があります。jobFinishedも併せて記述しておきます。

|メソッド名|役割|戻り値|
|:--|:--|:--|
|onStartJob|登録したジョブが実行されると呼び出されるメソッドです。このメソッドはメインスレッドで呼び出されるため、時間がかかる処理は別スレッドで実行します。|【trueの場合】<br>onStartJobメソッドが終了時点で処理が続いている場合は、trueを返します。もし、trueを返した場合は、処理の最後にjobFinishedメソッドを呼び出さなければなりません。<br>【falseの場合】<br>処理が継続していない場合に返す。|
|onStopJob|処理実行中に条件を満たせず、中断した場合に呼び出されるメソッドです。このメソッドの中でjobFinishedメソッドを呼び出す必要があります。|【trueの場合】<br>trueを設定するとback-offで設定した内容に併せてリスケジュールされます。<br>【falseの場合】<br>このジョブは終了します。|
|jobFinished|ジョブを終了するためのメソッドです。２つの引数をとります。|-|

### アクティビティ
アクティビティでは、JobInfoを生成しています。
JobInfoを生成するにはJobInfo.Builderを使用します。

```kotlin

val componentName = ComponentName(this,
        CasarealJobService::class.java)
```
まず初めに、JobServiceを継承したクラスの情報を含むComponentNameを生成します。
JobInfoに設定するために事前に準備しておきます。


```kotlin

val jobInfo = JobInfo.Builder(jobId, componentName)
        .apply {
            setBackoffCriteria(5000, JobInfo.BACKOFF_POLICY_LINEAR);
            setPersisted(false)
            setPeriodic(1000)
            setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE)
            setRequiresCharging(false)
        }.build()
```
JobInfo.Builderクラスのコンストラクタの第1引数には、Job特定するためのIDを指定し、第2引数には実際のJobであるComponetNameのオブジェクトを渡します。

JobInfo.Builderクラスで設定可能なプロパティは多く存在しますが、ここでは使用頻度が高そうなものだけをピックアップしています。
プロパティのセッターメソッドとその説明は以下のとおりです。

|プロパティ名|説明|
|:---|:---|
|setBackoffCriteria|バックオフの計算ポリシーと基準時間を設定します。|
|setPersisted|端末が再起動した場合、Jobを継続するかどうかを設定します。<br>もし、trueを設定した場合は、「uses-permissions」で「RECEIVE_BOOT_COMPLETED」を指定します。|
|setPeriodic|定期実行する場合の間隔（ミリ秒）を設定します。|
|setRequiredNetworkType|ネットワークに接続している必要があるか指定します。|
|setRequiresCharging|充電中している必要があるか指定します。|

その他にも多くのプロパティが存在します。下記を参照してください。
https://developer.android.com/reference/android/app/job/JobInfo.Builder.html

```kotlin
val scheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
```
JobSchedulerを取得しています。

```kotlin
scheduler.schedule(jobInfo)
```
JobSchedulerに登録し、実行します。

```kotlin
scheduler.cancel(jobId);

// 下記の方法だとアプリで登録したすべてのJobがキャンセルされる
scheduler.cancelAll()
```
JobScheduler#cancelで、　JobInfoに指定したID を利用してJobをキャンセルできます。
もし、すべてのJobをキャンセルするには、JobScheduler#cancelAllメソッドを呼び出します。


#### バックオフとは
JobSchedulerで出てくるBack-Offについて説明しておきます。
簡単に説明すると下記のイメージです。

![BackOff.png](https://qiita-image-store.s3.amazonaws.com/0/99960/0de0dad0-c191-5e30-3d78-3c4492e464f5.png)


要するに、条件が合わないから、処理を後回し（Back-Off）することです。

# まとめ
Oreoでバックグラウンド処理は、Serviceでは今まで通り動作しません。JobSchedulerを利用する必要がありますが、Serviceとは違い条件が一致するまで動作しないため、注意が必要そうです。