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.os.Bundle;
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.StartStopToken;
import androidx.work.impl.StartStopTokens;
import androidx.work.impl.WorkDatabase;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkGenerationalId;
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 WorkTimer mWorkTimer;
private final StartStopTokens mStartStopTokens = new StartStopTokens();
// Synthetic access
WorkManagerImpl mWorkManagerImpl;
public WorkManagerGcmDispatcher(
@NonNull WorkManagerImpl workManager, @NonNull WorkTimer timer) {
mWorkManagerImpl = workManager;
mWorkTimer = timer;
}
/**
* 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().executeOnTaskThread(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, "Handling task " + 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;
}
Bundle extras = taskParams.getExtras();
int generation = extras != null
? extras.getInt(GcmTaskConverter.EXTRA_WORK_GENERATION, 0) : 0;
WorkGenerationalId id = new WorkGenerationalId(workSpecId, generation);
WorkSpecExecutionListener listener = new WorkSpecExecutionListener(id,
mStartStopTokens);
StartStopToken startStopToken = mStartStopTokens.tokenFor(id);
WorkSpecTimeLimitExceededListener timeLimitExceededListener =
new WorkSpecTimeLimitExceededListener(mWorkManagerImpl, startStopToken);
Processor processor = mWorkManagerImpl.getProcessor();
processor.addExecutionListener(listener);
String wakeLockTag = "WorkGcm-onRunTask (" + workSpecId + ")";
PowerManager.WakeLock wakeLock = WakeLocks.newWakeLock(
mWorkManagerImpl.getApplicationContext(), wakeLockTag);
mWorkManagerImpl.startWork(startStopToken);
mWorkTimer.startTimer(id, AWAIT_TIME_IN_MILLISECONDS, timeLimitExceededListener);
try {
wakeLock.acquire();
listener.getLatch().await(AWAIT_TIME_IN_MINUTES, TimeUnit.MINUTES);
} catch (InterruptedException exception) {
Logger.get().debug(TAG, "Rescheduling WorkSpec" + workSpecId);
return reschedule(workSpecId);
} finally {
processor.removeExecutionListener(listener);
mWorkTimer.stopTimer(id);
wakeLock.release();
}
if (listener.needsReschedule()) {
Logger.get().debug(TAG, "Rescheduling WorkSpec" + 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, "WorkSpec %s does not exist" + workSpecId);
return GcmNetworkManager.RESULT_FAILURE;
} else {
switch (state) {
case SUCCEEDED:
case CANCELLED:
Logger.get().debug(TAG, "Returning RESULT_SUCCESS for WorkSpec " + workSpecId);
return GcmNetworkManager.RESULT_SUCCESS;
case FAILED:
Logger.get().debug(TAG, "Returning RESULT_FAILURE for WorkSpec " + workSpecId);
return GcmNetworkManager.RESULT_FAILURE;
default:
Logger.get().debug(TAG, "Rescheduling eligible work.");
return reschedule(workSpecId);
}
}
}
private int reschedule(@NonNull final String workSpecId) {
final WorkDatabase workDatabase = mWorkManagerImpl.getWorkDatabase();
workDatabase.runInTransaction(new Runnable() {
@Override
public void run() {
// 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());
}
});
Logger.get().debug(TAG, "Returning RESULT_SUCCESS for WorkSpec " + workSpecId);
return GcmNetworkManager.RESULT_SUCCESS;
}
static class WorkSpecTimeLimitExceededListener implements WorkTimer.TimeLimitExceededListener {
private static final String TAG = Logger.tagWithPrefix("WrkTimeLimitExceededLstnr");
private final WorkManagerImpl mWorkManager;
private final StartStopToken mStartStopToken;
WorkSpecTimeLimitExceededListener(
@NonNull WorkManagerImpl workManager,
@NonNull StartStopToken startStopToken) {
mWorkManager = workManager;
mStartStopToken = startStopToken;
}
@Override
public void onTimeLimitExceeded(@NonNull WorkGenerationalId id) {
Logger.get().debug(TAG, "WorkSpec time limit exceeded " + id);
mWorkManager.stopWork(mStartStopToken);
}
}
static class WorkSpecExecutionListener implements ExecutionListener {
private static final String TAG = Logger.tagWithPrefix("WorkSpecExecutionListener");
private final WorkGenerationalId mGenerationalId;
private final CountDownLatch mLatch;
private boolean mNeedsReschedule;
private final StartStopTokens mStartStopTokens;
WorkSpecExecutionListener(
@NonNull WorkGenerationalId generationalId,
@NonNull StartStopTokens startStopTokens) {
mGenerationalId = generationalId;
mStartStopTokens = startStopTokens;
mLatch = new CountDownLatch(1);
mNeedsReschedule = false;
}
boolean needsReschedule() {
return mNeedsReschedule;
}
CountDownLatch getLatch() {
return mLatch;
}
@Override
public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) {
if (!mGenerationalId.equals(id)) {
Logger.get().warning(TAG,
"Notified for " + id + ", but was looking for " + mGenerationalId);
} else {
mStartStopTokens.remove(id);
mNeedsReschedule = needsReschedule;
mLatch.countDown();
}
}
}
}