WorkManagerGcmDispatcher.java

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.work.impl.background.gcm;


import android.content.Context;
import android.os.PowerManager;

import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.work.Logger;
import androidx.work.WorkInfo;
import androidx.work.impl.ExecutionListener;
import androidx.work.impl.Processor;
import androidx.work.impl.Schedulers;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.WakeLocks;
import androidx.work.impl.utils.WorkTimer;

import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.TaskParams;

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

/**
 * Handles requests for executing {@link androidx.work.WorkRequest}s on behalf of
 * {@link WorkManagerGcmService}.
 */
public class WorkManagerGcmDispatcher {

    // Synthetic access
    static final String TAG = Logger.tagWithPrefix("WrkMgrGcmDispatcher");

    private static final long AWAIT_TIME_IN_MINUTES = 10;
    private static final long AWAIT_TIME_IN_MILLISECONDS = AWAIT_TIME_IN_MINUTES * 60 * 1000;

    private final Context mContext;
    private final WorkTimer mWorkTimer;

    // Synthetic access
    WorkManagerImpl mWorkManagerImpl;


    public WorkManagerGcmDispatcher(@NonNull Context context, @NonNull WorkTimer workTimer) {
        mContext = context.getApplicationContext();
        mWorkTimer = workTimer;
        mWorkManagerImpl = WorkManagerImpl.getInstance(context);
    }


    /**
     * Handles {@link WorkManagerGcmService#onInitializeTasks()}.
     */
    @MainThread
    public void onInitializeTasks() {
        // Reschedule all eligible work, as all tasks have been cleared in GCMNetworkManager.
        // This typically happens after an upgrade.
        mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(new Runnable() {
            @Override
            public void run() {
                Logger.get().debug(TAG, "onInitializeTasks(): Rescheduling work");
                mWorkManagerImpl.rescheduleEligibleWork();
            }
        });
    }

    /**
     * Handles {@link WorkManagerGcmService#onRunTask(TaskParams)}.
     */
    public int onRunTask(@NonNull TaskParams taskParams) {
        // Tasks may be executed concurrently but every Task will be executed in a unique thread
        // per tag, which in our case is a workSpecId. Therefore its safe to block here with
        // a latch because there is 1 thread per workSpecId.

        Logger.get().debug(TAG, String.format("Handling task %s", taskParams));

        String workSpecId = taskParams.getTag();
        if (workSpecId == null || workSpecId.isEmpty()) {
            // Bad request. No WorkSpec id.
            Logger.get().debug(TAG, "Bad request. No workSpecId.");
            return GcmNetworkManager.RESULT_FAILURE;
        }

        WorkSpecExecutionListener listener = new WorkSpecExecutionListener(workSpecId);
        WorkSpecTimeLimitExceededListener timeLimitExceededListener =
                new WorkSpecTimeLimitExceededListener(mWorkManagerImpl);
        Processor processor = mWorkManagerImpl.getProcessor();
        processor.addExecutionListener(listener);
        String wakeLockTag = String.format("WorkGcm-onRunTask (%s)", workSpecId);
        PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(mContext, wakeLockTag);
        mWorkManagerImpl.startWork(workSpecId);
        mWorkTimer.startTimer(workSpecId, AWAIT_TIME_IN_MILLISECONDS, timeLimitExceededListener);

        try {
            wakeLock.acquire();
            listener.getLatch().await(AWAIT_TIME_IN_MINUTES, TimeUnit.MINUTES);
        } catch (InterruptedException exception) {
            Logger.get().debug(TAG, String.format("Rescheduling WorkSpec %s", workSpecId));
            return reschedule(workSpecId);
        } finally {
            processor.removeExecutionListener(listener);
            mWorkTimer.stopTimer(workSpecId);
            wakeLock.release();
        }

        if (listener.needsReschedule()) {
            Logger.get().debug(TAG, String.format("Rescheduling WorkSpec %s", workSpecId));
            return reschedule(workSpecId);
        }

        WorkDatabase workDatabase = mWorkManagerImpl.getWorkDatabase();
        WorkSpec workSpec = workDatabase.workSpecDao().getWorkSpec(workSpecId);
        WorkInfo.State state = workSpec != null ? workSpec.state : null;

        if (state == null) {
            Logger.get().debug(TAG, String.format("WorkSpec %s does not exist", workSpecId));
            return GcmNetworkManager.RESULT_FAILURE;
        } else {
            switch (state) {
                case SUCCEEDED:
                case CANCELLED:
                    Logger.get().debug(TAG,
                            String.format("Returning RESULT_SUCCESS for WorkSpec %s", workSpecId));
                    return GcmNetworkManager.RESULT_SUCCESS;
                case FAILED:
                    Logger.get().debug(TAG,
                            String.format("Returning RESULT_FAILURE for WorkSpec %s", workSpecId));
                    return GcmNetworkManager.RESULT_FAILURE;
                default:
                    Logger.get().debug(TAG, "Rescheduling eligible work.");
                    return reschedule(workSpecId);
            }
        }
    }

    /**
     * Cleans up resources when the {@link WorkManagerGcmDispatcher} is no longer in use.
     */
    public void onDestroy() {
        mWorkTimer.onDestroy();
    }

    private int reschedule(@NonNull String workSpecId) {
        WorkDatabase workDatabase = mWorkManagerImpl.getWorkDatabase();
        workDatabase.beginTransaction();
        try {
            // Mark the workSpec as unscheduled. We are doing this explicitly here because
            // there are many cases where WorkerWrapper may not have had a chance to update this
            // flag. For e.g. this will happen if the Worker took longer than 10 minutes.
            workDatabase.workSpecDao()
                    .markWorkSpecScheduled(workSpecId, WorkSpec.SCHEDULE_NOT_REQUESTED_YET);
            // We reschedule on our own to apply our own backoff policy.
            Schedulers.schedule(
                    mWorkManagerImpl.getConfiguration(),
                    mWorkManagerImpl.getWorkDatabase(),
                    mWorkManagerImpl.getSchedulers());
            workDatabase.setTransactionSuccessful();
        } finally {
            workDatabase.endTransaction();
        }

        Logger.get().debug(TAG,
                String.format("Returning RESULT_SUCCESS for WorkSpec %s", workSpecId));
        return GcmNetworkManager.RESULT_SUCCESS;
    }

    static class WorkSpecTimeLimitExceededListener implements WorkTimer.TimeLimitExceededListener {
        private static final String TAG = Logger.tagWithPrefix("WrkTimeLimitExceededLstnr");

        private final WorkManagerImpl mWorkManager;

        WorkSpecTimeLimitExceededListener(@NonNull WorkManagerImpl workManager) {
            mWorkManager = workManager;
        }

        @Override
        public void onTimeLimitExceeded(@NonNull String workSpecId) {
            Logger.get().debug(TAG, String.format("WorkSpec time limit exceeded %s", workSpecId));
            mWorkManager.stopWork(workSpecId);
        }
    }

    static class WorkSpecExecutionListener implements ExecutionListener {
        private static final String TAG = Logger.tagWithPrefix("WorkSpecExecutionListener");
        private final String mWorkSpecId;
        private final CountDownLatch mLatch;
        private boolean mNeedsReschedule;

        WorkSpecExecutionListener(@NonNull String workSpecId) {
            mWorkSpecId = workSpecId;
            mLatch = new CountDownLatch(1);
            mNeedsReschedule = false;
        }

        boolean needsReschedule() {
            return mNeedsReschedule;
        }

        CountDownLatch getLatch() {
            return mLatch;
        }

        @Override
        public void onExecuted(@NonNull String workSpecId, boolean needsReschedule) {
            if (!mWorkSpecId.equals(workSpecId)) {
                Logger.get().warning(TAG,
                        String.format("Notified for %s, but was looking for %s", workSpecId,
                                mWorkSpecId));
            } else {
                mNeedsReschedule = needsReschedule;
                mLatch.countDown();
            }
        }
    }
}