techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

JobSchedulerの使い方

こちらもどうぞ

blog.techium.jp

JobSchedulerとは?

JobSchedulerはJob登録を行い、端末が指定した状態になったときにSystemに実行してもらう機能を持つAPIです。 ネットワークの状態や、給電状態、DeviceのIdle状態などを契機に設定が行え、再起動時にもよしなにしてくれる機能を持つ、 高機能なv21以降で利用できるAPIです。 support libraryには入ってないのでバックポートは無いが、AlarmManagerを利用するよりは定期処理が組みやすいのでオススメです。

使い方

ManifestへJobServiceの登録

JobSchedulerを利用する場合は、JobServiceを継承したJobの実行者を実装する必要があります。 まずはこのJobServiceをManifestに登録しましょう。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.kobashin.sample.jobschedulersample">
    
    <!-- setPersistedを利用する場合はBOOT_COMPLETEDの宣言が必要-->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <application ...>
        <activity android:name=".MainActivity">
            <!-- 省略 -->
        </activity>

        <!-- Required android.permission.BIND_JOB_SERVICE -->
        <service
            android:name=".MyJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="true"></service>
    </application>
</manifest>

ここでは、JobServiceを継承したクラスとしてMyJobServiceを作成しました。 ポイントは、MyJobServiceにandroid.permission.BIND_JOB_SERVICEを宣言している点です。 これが無いと、java.lang.IllegalArgumentException: Scheduled service does not require android.permission.BIND_JOB_SERVICE permission というエラーが発生します。

また、後述するsetPersistedを利用した再起動後にもScheduleを継続させる機能を利用する場合には、BOOT_COMPLETEDの宣言が必要です。

JobServiceの実装

JobServiceに実装すべきは、JobService.onStartJob()とJobService.onStopJob()メソッドです。

メソッド名 概要
onStartJob() ScheduleしたJobInfoの条件になった時に呼び出される。Jobの実処理を実装する。メインスレッドで呼び出されるので、Jobの実態は別スレッドで行うこと。
onStopJob() Jobの実行中にJobInfoの条件が崩れた場合に呼び出される。このメソッドが呼び出されるときはJobを止める時である。OSはjobFinished)()を呼び出すことを期待している。

サンプルでは、Jobとして別スレッドで10秒sleepする処理を入れています。

public class MyJobService extends JobService {

     /* 省略 */

    // ---- scheduled job interface

    @Override
    @DebugLog
    public boolean onStartJob(JobParameters params) {
        // 登録したJob実行タイミングで呼び出される
        // Jobの実処理を書く。ただし、メインスレッドで呼び出されるので、
        // 重たい処理は別Threadで行うこと。

        mParams = params;
        new Thread(new Runnable() {

            @Override
            @DebugLog
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (mParams != null) {
                    // jobFinished(JobParameters params, boolean needsReschedule)
                    // params: Job開始時にonStartJob()の引数で受け取ったparamsを指定
                    // needsReschedule:
                    //     true JobInfo.Builder.setBackoffCriteria()で指定したback-off criteriaに従って
                    //          JobをRescheduleする
                    //     false Resucheduleしない
                    jobFinished(mParams, false);
                }
            }
        }).start();


        // 処理が継続している場合はtrueを返す。trueを返した場合は、処理終了時にjobFinished()をコールすること
        // 特に何もしていない場合はfalseを返す。
        return true;
    }

    @Override
    @DebugLog
    public boolean onStopJob(JobParameters params) {
        // 要求したJobの実行中に条件を満たさなくなった場合に呼び出される
        // これが呼び出された場合は jobFinished() を呼び出すべきである
        jobFinished(params, false);

        // trueの場合、jobをback-off設定値に合わせてrescheduleする
        // returnした後の処理実行は保証されない
        return false;
    }
}

これで、JobがScheduleされた時に実行する部分を実装したことになります。

JobServiceの利用

JobをScheduleするとき、Cancelするときには、Scheduler.schedule() / Scheduler.cancel() / Scheduler.cancelAll()を利用します。 これらはメソッド名のままなので、詳細な説明は必要ないでしょう。

ScheduleするJobはJobInfoの形式で作成します。 JobInfoはBuilderを持っており、このBuilderを通じてJobの実行タイミングを制御できます。 各メソッドでどういったことができるかはコメントを厚めに入れているので、そこを参照してください。

サンプルでは、MyJobServiceのpublic staticなメソッドとして、schedule() / cancel()を作成しました。

public class MyJobService extends JobService {
    public MyJobService() {
    }

    private final static ComponentName JOB_SERVICE_NAME =
            new ComponentName("com.kobashin.sample.jobschedulersample",
                    "com.kobashin.sample.jobschedulersample.MyJobService");

    private final static int JOB_ID = 0x01;

    public static void cancelJobs(Context context){
        JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        scheduler.cancel(JOB_ID);

        //scheduler.cancelAll();
    }

    @DebugLog
    public static void schedule(Context context) {
        JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, JOB_SERVICE_NAME);

        // setBackOffCriteria(long initialBackoffMillis, int backoffPolicy)
        //     initialbackoffMillis: バックオフ時間算出の基準値
        //     backoffPolicy: BACKOFF_POLICY_LINEARかBACKOFF_POLICY_EXPONENTIALを指定
        //                    LINEARの時は current+initial * fail_count
        //                    EXPONENTIALの時は current + initial * 2 ^ (fail_count -1)
        //                    後にリトライされる。defaultは30sec, EXPONENTIAL。最長バックオフは5hr
        builder.setBackoffCriteria(10000, JobInfo.BACKOFF_POLICY_LINEAR);

        // setExtras(PersistableBundle)
        // PersistableBundleを利用して、onStartJob時に取り出すbundleを用意できる
        PersistableBundle bundle = new PersistableBundle();
        builder.setExtras(bundle);

        // setMinimumLatency()
        //     実行可能になってからの最低遅延時間を設定する
        //     定期実行Jobには必要ないため、build()時にエラー扱いとなる
//        builder.setMinimumLatency(5000);

        // setOverrideDeadline()
        //     実行可能になってからの最大遅延時間を設定する
        //     定期実行Jobには必要ないため、build()時にエラー扱いとなる
//        builder.setOverrideDeadline(20000);


        // setPeriodic()
        //     定期実行を設定する。前のJobが終わってからの経過時間(Millis)を指定する
        //     Priodic指定した場合は、状態が変更されても継続実行される
//        builder.setPeriodic(10000);

        // setPersisted()
        //     再起動時にJobを実行継続させるかどうか。
        //     trueを設定した場合は、BOOT_COMPLETEDが無いとエラー扱いとなる
        builder.setPersisted(true);


        // setRequiredNetworkType()
        //      Jobの実行に必要なネットワーク形態を設定する
        //      NETWORK_TYPE_NONE: 指定なし(ありでもなしでも)
        //      NETWORK_TYPE_ANY: なんらかのネットワーク
        //      NETWORK_TYPE_UNMETERD: 従量制でないネットワーク
        builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);

        // Deviceがidle maintenance windowの時に実行するかどうか
        // idolかつbackoffCriteriaを設定するとexceptionを拾う
//        builder.setRequiresDeviceIdle(true);


        // Deviceが給電状態かどうかを設定する
        builder.setRequiresCharging(true);

        scheduler.schedule(builder.build());
    }
    /* 省略 */
}

はまりポイント

setPersisted()を利用する際に、BOOT_COMPLETEDのパーミッションを入れてるのに下記エラーが発生する問題が起きた。 java.lang.IllegalArgumentException: Error: requested job be persisted without holding RECEIVE_BOOT_COMPLETED permission.

stackoverflow.com

内部で保持してるキャッシュが影響しているとのことなので、一度アプリケーションを再インストールすれば回避できた。

sample code

今回のサンプルコードはこちら。 Hugoを利用しているので@DebugLogなアノテーションが入ってます。

github.com