いまさらWorkManager〜基本編④（状態の確認とキャンセル）〜

# はじめに
この[記事](https://qiita.com/naoi/items/e08f081fe42ef3694046)で説明した通り、様々な機能を活用していきましょう。
WorkManagerのキューに追加して、処理を実行下はいいけど、何かしらのタイミングや条件で処理をキャンセルしたい場合があります。
また、Workの状態を何かしらの条件を使いたいなんてことも大いにあります。
今回は、状態の確認方法やキャンセルの方法を説明していきます。

# 動作環境
この記事の動作環境は以下のとおりです。

* Android Studio：3.3
* Kotln：1.3.11
* Open JDK:1.8
* compileSdkVersion：28
* targetSdkVersion：28
* minSdkVersion：19

# 目標

* キューに追加したWorkerの情報を取得する
* Workerの状態を確認する
* Workerをキャンセルする

# 説明
## Workerの保持する情報

定期的に実行する処理はWorkerクラスを継承して定義していきます。
このWorkerクラスは、様々な情報を保持しています。
その中に、UUID(UUIDとは、重複のない一意のユニークなIDを取得するためのクラス)やタグがあります。
UUIDやタグはキューに追加したWorkerを取得する際に使用します。

イメージにするとこんな感じになります。

![Worker詳細.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/8ba2df6b-ff7d-b08b-edde-089ae6b96caf.png)


## WorkRequestに情報を追加する

キューに登録したWorkerを取得するには、WorkRequestのオブジェクトを生成するタイミングでタグ設定、WorkRequestのオブジェクト生成時に作られたUUDIを取得しておく必要があります。

タグを設定するには、Builderクラスで行います。

```kotlin
val periodicWork = PeriodicWorkRequest.Builder(
    MyWorker::class.java,
    Duration.ofMinutes(15)
).apply {
    addTag(workerTag)
}.build()
```

UUIDを取得するには、Builderでビルドしたあとに取得します。

```kotlin
private var workerId: UUID? = null

workerId = periodicWork.id
```

これらを取得することによってキューに追加されてもWorkerクラスのオブジェクトの情報が取得できます。

## キューに追加したWorkerの情報を取得する

キューについかしたWorkerの情報は先に記述した通り、タグとUUIDを利用してうけとります。
WorkManagerクラスのメソッドを利用します。

|メソッド名|引数|戻り値|
|-----|----|----|
|getWorkInfoById|UUID|ListenableFuture<WorkInfo>|
|getWorkInfosByTag|String|ListenableFuture<List<WorkInfo>>|

戻り値の方がどちらも **ListenableFuture**となっています。
このクラスはGoogleが用意したAPIですので、詳細は[API](https://google.github.io/guava/releases/19.0/api/docs/com/google/common/util/concurrent/ListenableFuture.html)を確認してください。
そのため、これらのメソッドで取得したままではWorkInfoは利用できないので、ListenableFutureからWorkInfoやListを取得する必要があります。

イメージとしては、下図のようになります。


![WorkInfoの取得.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/0fd6d279-c9a7-ac3b-55fc-b529c47ce3d3.png)


それぞれのメソッドで取得するコードの例も確認してみます。

```kotlin
val info = manager.getWorkInfoById(workerId!!)
val infoByTags = manager.getWorkInfosByTag(workerTag)
```

## Workerの状態を確認する

getWorkInfoByXXXで取得したWorkInfoにWorkの状態が格納されています。状態はstateプロパティとして格納されています。

状態はWorkクラス内にEnumで宣言されています。どの様な状態があるか確認します。

 **WorkInfo.State** 

|状態|説明|
|----|-----|
| BLOCKED |前提条件を満たしていないため、実行出来ていない状態を表します。|
| CANCELLED |キャンセルされた状態を表します。関連しているWorkRequestもキャンセルと扱われ、関連するWorkRequestも実行されません。※実行の関連は、別の記事で紹介します。|
| ENQUEUED |前提条件が満たされており、実行可能状態でキューについかされている状態を表します。|
| FAILED |処理が失敗したことを表します。関連しているWorkRequestも失敗と扱われ、関連するWorkRequestも実行されません。※実行の関連は、別の記事で紹介します。|
| RUNNING |処理が実行中であることを表します。|
| SUCCEEDED |処理が正常終了したことを表します。PeriodicWorkRequest（定期的に実行）の場合は、この状態にはならず、ENQUEUEDに遷移し、再度実行されます。|

説明からもわかるように、stateプロパティは実行状態によって変化します。
また、WorkRequstの種類によっても異なります。

フロー自体は正確ではありませんが、状態のイメージを掴むためにフロー図にしてみました。

![WorkInfo状態遷移.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/d2640caf-624e-e097-7300-9f0537bae528.png)


しっかりと状態が確認できれば、何故動いていないかなどのデバッグに役立つと思います。

実際にコードでも確認しておきましょう。

```kotlin
val info = manager.getWorkInfoById(workerId!!)
Toast.makeText(this, "State:${info.get().state}", Toast.LENGTH_LONG).show()

val infoByTags = manager.getWorkInfosByTag(workerTag)
infoByTags.get().forEach {
    Toast.makeText(this, "State:${it.state}", Toast.LENGTH_LONG).show()
}
```

## キューに追加したWorkRequestキャンセルする
キューにWorkRequestをキャンセルするには、キューに登録したWorkRequestのIDやタグを指定してキャンセルします。WorkManagerのメソッドをキャンセルします。
キャンセルするWorkRequestを特定するための情報によって、メソッド名が異なります。

 **メソッド一覧（一部抜粋）** 

|メソッド名|引数|説明|
|----|-----|----|
| cancelAllWork |なし|未完了のWorkRequestをすべてキャンセルします。しかし、アプリに悪影響を及ぼし、意図しない不具合が発生する可能性があります。このメソッドはおすすめしません。（公式サイトでおすすめされていません。）|
| cancelAllWorkByTag |タグ名:String|タグが指定されているWorkRequestをすべてキャンセルします。すでに実行中の場合は、継続する可能性があります。また、関連するWorkRequestでは、Workerクラスを継承したクラス内にオーバーライドしたonStoppedメソッドが呼ばれます。|
| cancelWorkById |WorkのID:UUID|基本的な動作は「cancelAllWorkByTag」と同じです。唯一の違いは、UUIDで取得できたWorkをキャンセルします。|

実装としては、Workコードでも確認しておきます。

```kotlin
manager.cancelWorkById(workerId!!)

manager.cancelAllWorkByTag(workerTag)
```

# 実装コード
この記事で動作確認したコードをすべて掲載します。
AndroidManifest.xmlはテンプレートで作成された状態から変更していないため、省略します。

```groovy:build.gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "jp.co.casareal.workmanagerstateandcancel"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    def work_version = "1.0.0"

    implementation "android.arch.work:work-runtime-ktx:$work_version"

}
```

```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:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <Button
            android:id="@+id/btnStart"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Workスタート"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintHorizontal_bias="0.498" app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="188dp"
            android:layout_marginBottom="51dp" app:layout_constraintBottom_toTopOf="@+id/btnState"/>
    <Button
            android:text="状態表示"
            android:layout_width="158dp"
            android:layout_height="47dp"
            android:id="@+id/btnState"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btnStart"
            app:layout_constraintBottom_toTopOf="@+id/btnCancel" app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintVertical_bias="0.586"/>
    <Button
            android:text="キャンセル"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnCancel"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginEnd="8dp" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp" android:layout_marginBottom="140dp"
            app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.498"
            android:layout_marginTop="107dp" app:layout_constraintTop_toBottomOf="@+id/btnState"/>
</android.support.constraint.ConstraintLayout>
```

```kotlin:MyWork.kt
package jp.co.casareal.workmanagerstateandcancel

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.support.v4.app.NotificationCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.lang.Exception
import java.text.SimpleDateFormat

class MyWorker(cxt: Context, params: WorkerParameters) : Worker(cxt, params) {
    val notificationManager =
        applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    // カテゴリー名（通知設定画面に表示される情報）
    val name = "通知のタイトル的情報を設定"
    // システムに登録するChannelのID
    val id = "casareal_chanel"
    // 通知の詳細情報（通知設定画面に表示される情報）
    val notifyDescription = "この通知の詳細情報を設定します"

    private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

    companion object {
        var nid = 1;
    }


    init {
        // Channelの取得と生成
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.getNotificationChannel(id) == null
            val mChannel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH)
            mChannel.apply {
                description = notifyDescription
            }
            notificationManager.createNotificationChannel(mChannel)
        }
    }

    override fun doWork(): Result {

        for (i in 0..3) {

            val notification = NotificationCompat.Builder(applicationContext, id).apply {

                setContentText("${nid}回目のメッセージ:${simpleDateFormat.format(System.currentTimeMillis())}")
                setSmallIcon(R.drawable.ic_launcher_background)
            }

            notificationManager.notify(MyWorker.nid, notification.build())

            MyWorker.nid++
        }
        return Result.success()
    }
}
```

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

import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.Toast
import androidx.work.Constraints
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import kotlinx.android.synthetic.main.activity_main.*
import java.time.Duration
import java.util.*

class MainActivity : AppCompatActivity() {


    private val manager = WorkManager.getInstance()
    private var workerId: UUID? = null
    private val workerTag = "myworktag"

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



        btnStart.setOnClickListener {

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

                val periodicWork = PeriodicWorkRequest.Builder(
                    MyWorker::class.java,
                    Duration.ofSeconds(20)
                ).apply {
                    addTag(workerTag)

                }.build()

                workerId = periodicWork.id

                manager.enqueue(periodicWork)
            }
        }

        btnState.setOnClickListener {
            // キューに追加したWorkerの取得
            val info = manager.getWorkInfoById(workerId!!)
            Toast.makeText(this, "State:${info.get().state}", Toast.LENGTH_LONG).show()

            val infoByTags = manager.getWorkInfosByTag(workerTag)
            infoByTags.get().forEach {
                Toast.makeText(this, "State:${it.state}", Toast.LENGTH_LONG).show()
            }
        }

        btnCancel.setOnClickListener {
            manager.cancelWorkById(workerId!!)

            manager.cancelAllWorkByTag(workerTag)
        }
    }
}
```

# まとめ
この記事では以下が理解できました。

* キューに追加したWorkRequestはWorkManagerから取得が可能です。
* WorkInfoオブジェクトからWorkRequestの状態を取得が可能です。
* WorkManagerにはキャンセルするためのメソッドが用意されている。
![Bad Request]()