GcmScheduler.java

package com.birbit.android.jobqueue.scheduling;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;

import com.birbit.android.jobqueue.BatchingScheduler;
import com.birbit.android.jobqueue.log.JqLog;
import com.birbit.android.jobqueue.network.NetworkUtil;
import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.OneoffTask;
import com.google.android.gms.gcm.Task;
import com.google.android.gms.gcm.TaskParams;

import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

class GcmScheduler extends Scheduler {
    private static final String KEY_UUID = "uuid";
    private static final String KEY_ID = "id";
    private static final String KEY_DELAY = "delay";
    private static final String KEY_NETWORK_STATUS = "networkStatus";
    private static SharedPreferences preferences;
    private final GcmNetworkManager gcmNetworkManager;
    private final Class<? extends GcmJobSchedulerService> serviceClass;

    GcmScheduler(Context context, Class<? extends GcmJobSchedulerService> serviceClass) {
        this.serviceClass = serviceClass;
        gcmNetworkManager = GcmNetworkManager.getInstance(context.getApplicationContext());
    }

    private static SharedPreferences getPreferences(Context context) {
        synchronized (GcmScheduler.class) {
            if (preferences == null) {
                preferences = context.getSharedPreferences("jobqueue_gcm_scheduler",
                        Context.MODE_PRIVATE);
            }
            return preferences;
        }
    }

    /**
     * Creates a new ID for the job info. Can be overridden if you need to provide different ids not
     * to conflict with the rest of your application.
     *
     * @return A unique integer id for the next Job request to be sent to system scheduler
     */
    @SuppressLint("CommitPrefEdits")
    private int createId() {
        synchronized (GcmScheduler.class) {
            final SharedPreferences preferences = getPreferences(getApplicationContext());
            final int id = preferences.getInt(KEY_ID, 0) + 1;
            preferences.edit().putInt(KEY_ID, id).commit();
            return id;
        }
    }

    @Override
    public void request(SchedulerConstraint constraint) {
        if (JqLog.isDebugEnabled()) {
            JqLog.d("creating gcm wake up request for %s", constraint);
        }
        long endTimeMs = constraint.getOverrideDeadlineInMs() == null
                ? constraint.getDelayInMs() + TimeUnit.SECONDS.toMillis(getExecutionWindowSizeInSeconds())
                : constraint.getOverrideDeadlineInMs();
        OneoffTask oneoffTask = new OneoffTask.Builder()
                .setExecutionWindow(TimeUnit.MILLISECONDS.toSeconds(constraint.getDelayInMs()),
                        TimeUnit.MILLISECONDS.toSeconds(endTimeMs))
                .setRequiredNetwork(toNetworkState(constraint.getNetworkStatus()))
                .setPersisted(true)
                .setService(serviceClass)
                .setTag("jobmanager-" + createId())
                .setExtras(toBundle(constraint))
                .build();
        gcmNetworkManager.schedule(oneoffTask);
    }

    /**
     * GCMNetworkManager accepts an execution window for jobs so that it can batch them together for
     * better battery utilization. You can override this method to provide a different execution
     * window. The default value is {@link BatchingScheduler#DEFAULT_BATCHING_PERIOD_IN_MS} (converted
     * to seconds).
     * <p>
     * If this scheduling request is made for a Job with a deadline, this method is NOT called.
     *
     * @return The execution window time for the Job request
     */
    private long getExecutionWindowSizeInSeconds() {
        return TimeUnit.MILLISECONDS.toSeconds(BatchingScheduler.DEFAULT_BATCHING_PERIOD_IN_MS);
    }

    @Override
    public void cancelAll() {
        gcmNetworkManager.cancelAllTasks(serviceClass);
    }

    private static int toNetworkState(@NetworkUtil.NetworkStatus int networkStatus) {
        switch (networkStatus) {
            case NetworkUtil.DISCONNECTED:
                return Task.NETWORK_STATE_ANY;
            case NetworkUtil.METERED:
                return Task.NETWORK_STATE_CONNECTED;
            case NetworkUtil.UNMETERED:
                return Task.NETWORK_STATE_UNMETERED;
        }
        JqLog.e("unknown network status %d. Defaulting to CONNECTED", networkStatus);
        return Task.NETWORK_STATE_CONNECTED;
    }

    private static Bundle toBundle(SchedulerConstraint constraint) {
        Bundle bundle = new Bundle();
        // put boolean is api 22
        bundle.putString(KEY_UUID, constraint.getUuid());
        bundle.putInt(KEY_NETWORK_STATUS, constraint.getNetworkStatus());
        bundle.putLong(KEY_DELAY, constraint.getDelayInMs());
        return bundle;
    }

    private static SchedulerConstraint fromBundle(Bundle bundle) {
        SchedulerConstraint constraint = new SchedulerConstraint(bundle.getString(KEY_UUID));
        if (constraint.getUuid() == null) {
            // backward compatibility
            constraint.setUuid(UUID.randomUUID().toString());
        }
        constraint.setNetworkStatus(bundle.getInt(KEY_NETWORK_STATUS, NetworkUtil.DISCONNECTED));
        constraint.setDelayInMs(bundle.getLong(KEY_DELAY, 0));
        return constraint;
    }

    int onStartJob(TaskParams taskParams) {
        SchedulerConstraint constraint = fromBundle(taskParams.getExtras());
        if (JqLog.isDebugEnabled()) {
            JqLog.d("starting job %s", constraint);
        }

        ResultCallback callback = new ResultCallback();
        constraint.setData(callback);
        start(constraint);
        return callback.get() ? GcmNetworkManager.RESULT_RESCHEDULE : GcmNetworkManager.RESULT_SUCCESS;
    }

    @Override
    public void onFinished(SchedulerConstraint constraint, boolean reschedule) {
        Object data = constraint.getData();
        if (JqLog.isDebugEnabled()) {
            JqLog.d("finished job %s", constraint);
        }
        if (data instanceof ResultCallback) {
            ResultCallback callback = (ResultCallback) data;
            callback.onDone(reschedule);
        }
    }

    private static class ResultCallback {
        volatile boolean reschedule;
        CountDownLatch latch;

        ResultCallback() {
            latch = new CountDownLatch(1);
            reschedule = false;
        }

        public boolean get() {
            try {
                latch.await(10 * 60, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                JqLog.e("job did not finish in 10 minutes :/");
            }
            return reschedule;
        }

        void onDone(boolean reschedule) {
            this.reschedule = reschedule;
            latch.countDown();
        }
    }
}