WorkManager に enqueue されている Worker クラスをアプリのアップデート時にリネームした場合の挙動

# WorkManager に enqueue されている Worker クラスをアプリのアップデート時にリネームした場合の挙動

## 結果
`WorkerResult.FAILURE` 扱いになり、二度と再実行されない
**→ enqueue 状態の Worker クラスのリネーム（パッケージ移動を含む）には注意**

## 背景
WorkManager の内部処理を追っていると、Room に Worker の各種情報（ `WorkSpec` ）を積んでいることがわかりました。
ふと、 **「enqueue されている状態で、Worker の名前がアップデートにより変わったらどうなるんだろう？」** と思い、調査をしました。

## アップデート前のコード（例：v1.0.0）
```java
class MainActivity : AppCompatActivity() {

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

        val workManager = WorkManager.getInstance()
        fab.setOnClickListener {
            workManager.enqueue(OneTimeWorkRequest.Builder(Worker1::class.java).build())
            workManager.enqueue(OneTimeWorkRequest.Builder(Worker2::class.java).build())
        }
    }

    class Worker1 : Worker() {
        override fun doWork(): WorkerResult {
            Log.d("★", javaClass.simpleName)
            return WorkerResult.RETRY
        }
    }

    class Worker2 : Worker() {
        override fun doWork(): WorkerResult {
            Log.d("★", javaClass.simpleName)
            return WorkerResult.RETRY
        }
    }
}
```
FAB をタップしたら、無限にリトライする `Worker1` と `Worker2` を enqueue します。

### このときの DB の状態
<img width="835" alt="スクリーンショット 2018-06-23 0.01.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/9695/72dd147e-56fc-b1a2-fa4b-2dd686d15f41.png">
`WorkSpec` テーブルに `Worker1`, `Worker2` とも `state == 0` で積まれています。
※ state = {0: `ENQUEUED`, 1: `RUNNING`, 2: `SUCCEEDED`, 3: `FAILED`, 4: `BLOCKED`, 5: `CANCELLED`}

### このときの Log
```
(略) D/★: Worker1
(略) D/★: Worker2
(略) D/★: Worker1
(略) D/★: Worker2
　:
　:
```
`Worker1`, `Worker2` ともに無限にリトライされています。

## アップデート時の差分（例：v1.0.1）
`Worker2` を `Worker3` にリネーム
（Typo してると、Rename したくなりますよね）

```diff
...
        fab.setOnClickListener {
            workManager.enqueue(OneTimeWorkRequest.Builder(Worker1::class.java).build())
-           workManager.enqueue(OneTimeWorkRequest.Builder(Worker2::class.java).build())
+           workManager.enqueue(OneTimeWorkRequest.Builder(Worker3::class.java).build())
        }
...

-    class Worker2 : Worker() {
+    class Worker3 : Worker() {
        override fun doWork(): WorkerResult {
...
```

### アップデート後の Log（起動しただけ、新たに enqueue しない）
```
(略) D/★: Worker1
(略) D/★: Worker1
(略) D/★: Worker1
　:
　:
```
`Worker1` しか実行されていません。。。

### このときの DB の状態
<img width="791" alt="スクリーンショット 2018-06-23 0.02.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/9695/13bc1a76-3829-eceb-1fbf-ef54fd5ecb9d.png">
いつの間にか、 `Worker2` が `state == 3(FAILED)` になっていますね…
いつ失敗扱いになったのか見ていきます。

## 実装を見てみる
### Worker#doWork の呼び出し元
```java:WorkerWrapper.java
public void run() {
    ...
    if (mWorker == null) {
        mWorker = workerFromWorkSpec(mAppContext, mWorkSpec, extras); // ← mWorker の生成（この中を見る）
    }

    if (mWorker == null) {
        Log.e(TAG, String.format("Could for create Worker %s", mWorkSpec.workerClassName));
        setFailedAndNotify(); // ← mWorker の生成に失敗したら Failed にしている
        return;
    }
    ...
    Worker.WorkerResult result;
    try {
        result = mWorker.doWork(); // ← ここで doWork を呼び出し
    } catch (Exception | Error e) {
        result = Worker.WorkerResult.FAILURE;
    }
    ...
}
```

### WorkerWrapper#workerFromWorkSpec の実装
```java:WorkerWrapper.java
static Worker workerFromWorkSpec(...) {
    String workerClassName = workSpec.workerClassName;
    String workSpecId = workSpec.id;
    return workerFromClassName( // ←　この中を見る
            context,
            workerClassName,
            workSpecId,
            extras);
    }
```

### WorkerWrapper#workerFromClassName の実装
```java:WorkerWrapper.java
public static Worker workerFromClassName(...) {
    Context appContext = context.getApplicationContext();
    try {
        Class<?> clazz = Class.forName(workerClassName);
        Worker worker = (Worker) clazz.newInstance();
            ...
        return worker; // 成功時
    } catch (Exception e) {
        Log.e(TAG, "Trouble instantiating " + workerClassName, e);
    }
    return null; // 失敗時
}
```
**→　リフレクションによるインスタンス生成に失敗したら null を返していますね！**
つまり、 DB 内の `Worker2` のインスタンス化に失敗して、 `setFailedAndNotify()` で `FAILED` 扱いになります。

# まとめ
WorkManager の Worker クラスをアプリのアップデート時にリネームした場合の挙動

* `WorkerResult.FAILURE` 扱いになり、二度と再実行されない
* **→ enqueue 状態の Worker クラスのリネーム（パッケージ移動を含む）には注意**

# つぶやき
* どうしてもリネーム（パッケージ移動を含む）必要があった場合、Room の マイグレーションを行ったらなんとかなる...？
* Worker クラスは Proguard による難読化がかる...？そうすると、アップデート後も DB に格納されている値からリフレクションによるインスタンス生成は可能？