前回の記事でWorkerを複数起動するといろいろと弊害がありそうだと書いたが、今回はWorkerの状態を調べて複数起動を防ぐ方法を記す。
WorkInfoの取得
Workerの稼働状況はWorkInfoというオブジェクトに記録してあり、それをWorkManagerで取得することができる。
メソッド | 戻り値 | 説明 |
---|---|---|
getWorkInfoById(UUID) | ListenableFuture<WorkInfo> | IDでWorkInfoを取得 |
getWorkInfosByTag(tag) | ListenableFuture<List<WorkInfo>> | タグでWorkInfoを取得 |
戻り値はどちらもListenableFutureとなっていて、そのままではWorkInfoを利用できないのでListenableFutureからget()メソッドでWorkInfoやListを取り出してやらなければならない。なお、タグで取得するデータがListになっているのは、複数のWorkRequestに同じタグを設定できるためだ。
Workerの状態
Workerの状態はWorkInfoのstateプロパティで知ることができる。
状態 | 説明 |
---|---|
BLOCKED | 前提条件(制約)を満たしていないため、実行できない状態 |
CANCELLED | キャンセルされた状態 |
ENQUEUED | 前提条件が満たされており、実行可能状態でキューに追加されている状態 |
FAILED | 処理が失敗した状態 |
RUNNING | 処理が実行中 |
SUCCEEDED | 処理が正常終了した状態 |
定期実行の場合、処理の成功・失敗に関わらず、再スケジュールされてENQUEUED状態に移行するようなのでCANCELEDでなければ稼働中とみなしても良いと思われる。
サンプルプログラム
というわけで、サンプルアプリを作ってみた。
基本的には前回のサンプルと同じだが、Workerの稼働状況をチェックするメソッドを追加して、startWork()メソッドの最初で稼働中かどうか調べるようにしてみた
レイアウト
ActivityMain.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Worker Start"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Worker Stop"/>
</LinearLayout>
</RelativeLayout>
JAVAファイル
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//定数
final private static String LOG_TAG = "worker_sample";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = findViewById(R.id.button1);
button1.setOnClickListener(this);
Button button2 = findViewById(R.id.button2);
button2.setOnClickListener(this);
}
/* Workerを起動 */
private void startWrok (int num) {
//Wrokerが稼働中かチェック
// 稼働中なら終了
WorkManager manager = WorkManager.getInstance(this);
if (isWorkerEnqueued(manager,SampleWorker.WORK_TAG)) {
Log.d(LOG_TAG, "Worker already enqueued");
return;
}
//Workerに渡すデータを作成
Data data = new Data.Builder()
.putInt("num", num)
.build();
//制約(起動条件)を設定
// このサンプルでは制約なしでもいいが
// とりあえずバッテリー容量が少ないときはNGとした
Constraints constraints = new Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build();
//WorkRequestの作成
// PeriodicWorkRequestを使って20分毎に定期実行
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(
SampleWorker.class, 20, TimeUnit.MINUTES)
.setConstraints(constraints)
.setInputData(data)
.addTag(SampleWorker.WORK_TAG)
.build();
//キューに登録(スケジューリング)
manager.enqueue(request);
UUID uuid = request.getId();
ListenableFuture<WorkInfo> future = manager.getWorkInfoById(uuid);
Log.d(LOG_TAG, "Worker scheduled");
}
/* Workerを停止 */
private void stopWork() {
WorkManager manager = WorkManager.getInstance(this);
manager.cancelAllWorkByTag(SampleWorker.WORK_TAG);
Log.d(LOG_TAG, "Worker stopped");
}
/* ---------------------------------------------
Workerの状態を取得する
引数:workManager = WorKManagerのインスタンス
tag = タグ
戻値:true = 稼働中
false = 非稼働
----------------------------------------------- */
private boolean isWorkerEnqueued(WorkManager workManager, String tag) {
ListenableFuture<List<WorkInfo>> future = workManager.getWorkInfosByTag(tag);
try {
for (WorkInfo workInfo : future.get()){
//workInfoからstateを取得して状態をチェック
if (workInfo.getState() != WorkInfo.State.CANCELLED) {
//CANCELLED以外だったら稼働中とみなす
return true;
};
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
return false;
}
return false;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
startWrok(2); //Workerを起動
break;
case R.id.button2:
stopWork(); //Workerのキャンセル
break;
default:
break;
}
}
}
SampleWorker.java
public class SampleWorker extends Worker {
//定数
final private static String LOG_TAG = "worker_sample";
final public static String WORK_TAG = "SampleWorkerTAG";
//コンストラクタ
public SampleWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
//データを取得
Data data = getInputData();
int num = data.getInt("num", 1);
//出力テキストの作成
//numの個数だけ"Beautiful"というテキストを出力
StringBuilder sb = new StringBuilder();
for (int i=0; i<num; i++) {
sb.append("Beautiful ");
}
sb.append(getId().toString());
String output = sb.toString().trim();
//出力
Log.d(LOG_TAG,output); //ログに出力
return Result.success();
}
}
前回と同じくアプリを起動すると次の画面が開く。
動作も前回と同じなのだが、一応書いておくと、「WORKER START」ボタンをクリックするとWorkerがスケジューリングされ、20分毎にデバッグ用ログが出力される。「WORKER STOP」ボタンをクリックするとスケジューリングされているWorkerがキャンセルされる。ログはアクティビティ上には表示されないため何も動いていないように見えるが裏ではログが吐き出されている。前回と違うのは「WORKER START」を連続でクリックしてもWrokerが複数起動しないところだ。
実行結果のログは次の通りになる。
15:35:27.930 3338-3338/net.fineblue206.workersample D/worker_sample: Worker scheduled 15:35:28.054 3338-6207/net.fineblue206.workersample D/worker_sample: Beautiful Beautiful 8dec5e07-142f-464e-bcc2-8b549007828a 15:35:37.050 3338-3338/net.fineblue206.workersample D/worker_sample: Worker already enqueued 15:37:56.502 3338-3338/net.fineblue206.workersample D/worker_sample: Worker already enqueued 15:55:28.155 3338-4975/net.fineblue206.workersample D/worker_sample: Beautiful Beautiful 8dec5e07-142f-464e-bcc2-8b549007828a 16:15:28.289 3338-6176/net.fineblue206.workersample D/worker_sample: Beautiful Beautiful 8dec5e07-142f-464e-bcc2-8b549007828a 16:23:16.707 3338-3338/net.fineblue206.workersample D/worker_sample: Worker stopped
「START WORK」でWorkerを動かした後に二回ボタンをクリックしているが、既に同じタグのWorkerがキューに登録されているので起動を中止して複数起動を防いでいる。そして最初に動かしたWorkerだけが20分間隔で実行されている。