preface
Jetpack provides workManagers to perform some tasks, and since executing tasks involves task creation, task properties, task execution conditions, how to perform tasks, and so on. This article demystifies workManager through task attributes, task constraints, and how to perform tasks
Task Creation (Task Attributes)
// Task definition
class UploadWorker(conText: Context, params: WorkerParameters) : Worker(conText, params) {
override fun doWork(a): Result {
var name = inputData.getString("name")
Log.d("UploadWorker ==" ,"Dowork performed"+name)
Toast.maketext (applicationContext, "execute requestManager", toast.length_long).show()
return Result.retry() //Result.failure() Result.retry()}}Copy the code
As you can see, UploadWorker inherits Worker, which in turn inherits ListenableWorker, passing in two parameters: context and WorkerParameters
public abstract class Worker extends ListenableWorker {
// Package-private to avoid synthetic accessor.
SettableFuture<Result> mFuture;
@Keep
@SuppressLint("BanKeepAnnotation")
public Worker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@WorkerThread
public abstract @NonNull Result doWork();
// The startWork method executes the doWork method in the thread pool and sets the return value of the doWork method to return in the Future
@Override
public final @NonNull ListenableFuture<Result> startWork() {
mFuture = SettableFuture.create();
getBackgroundExecutor().execute(new Runnable() {
@Override
public void run() {
try {
Result result = doWork();
mFuture.set(result);
} catch(Throwable throwable) { mFuture.setException(throwable); }}});returnmFuture; }}Copy the code
The Worker code is also simple, exposing the doWork () method for subclasses to implement, and then providing the startWork method to perform doWork with a thread pool and put the return value into a SettableFuture.
//ListenableWorker
public abstract class ListenableWorker {
/ / context
private @NonNull Context mAppContext;
// Pass in parameters
private @NonNull WorkerParameters mWorkerParams;
// Whether to pause
private volatile boolean mStopped;
// Whether it is used
private boolean mUsed;
// Whether to run in foreground
private boolean mRunInForeground;
// The constructor context and WorkerParameters cannot be null; an exception is thrown when null
public ListenableWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
// Actually make sure we don't get nulls.
if (appContext == null) {
throw new IllegalArgumentException("Application Context is null");
}
if (workerParams == null) {
throw new IllegalArgumentException("WorkerParameters is null");
}
mAppContext = appContext;
mWorkerParams = workerParams;
}
public final @NonNull Context getApplicationContext() {
return mAppContext;
}
public final @NonNull UUID getId() {
return mWorkerParams.getId();
}
public final @NonNull Data getInputData() {
return mWorkerParams.getInputData();
}
public final @NonNull Set<String> getTags() {
return mWorkerParams.getTags();
}
@RequiresApi(24)
public final @NonNull List<Uri> getTriggeredContentUris() {
return mWorkerParams.getTriggeredContentUris();
}
@RequiresApi(24)
public final @NonNull List<String> getTriggeredContentAuthorities() {
return mWorkerParams.getTriggeredContentAuthorities();
}
@RequiresApi(28)
public final @Nullable Network getNetwork() {
return mWorkerParams.getNetwork();
}
@IntRange(from = 0)
public final int getRunAttemptCount() {
return mWorkerParams.getRunAttemptCount();
}
// The startWork method is implemented by the Woker class, where the call needs to be mainThread
@MainThread
public abstract @NonNull ListenableFuture<Result> startWork();
// Update the execution progress
@NonNull
public final ListenableFuture<Void> setProgressAsync(@NonNull Data data) {
return mWorkerParams.getProgressUpdater()
.updateProgress(getApplicationContext(), getId(), data);
}
@NonNull
public final ListenableFuture<Void> setForegroundAsync(@NonNull ForegroundInfo foregroundInfo) {
mRunInForeground = true;
return mWorkerParams.getForegroundUpdater()
.setForegroundAsync(getApplicationContext(), getId(), foregroundInfo);
}
// Suspend related operations
public final boolean isStopped() {
return mStopped;
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final void stop() {
mStopped = true;
onStopped();
}
public void onStopped() {
// Do nothing by default.
}
// Whether it is already in use
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final boolean isUsed() {
return mUsed;
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final void setUsed() {
mUsed = true;
}
// Whether to run in foreground
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public boolean isRunInForeground() {
return mRunInForeground;
}
/ * * *@hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @NonNull Executor getBackgroundExecutor() {
return mWorkerParams.getBackgroundExecutor();
}
/ * * *@hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @NonNull TaskExecutor getTaskExecutor() {
return mWorkerParams.getTaskExecutor();
}
/ * * *@hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @NonNull WorkerFactory getWorkerFactory() {
return mWorkerParams.getWorkerFactory();
}
/ / the execution result result/Success/Failure/Retry
public abstract static class Result {
@NonNull
public static Result success() {
return new Success();
}
@NonNull
public static Result success(@NonNull Data outputData) {
return new Success(outputData);
}
@NonNull
public static Result retry() {
return new Retry();
}
@NonNull
public static Result failure() {
return new Failure();
}
@NonNull
public static Result failure(@NonNull Data outputData) {
return new Failure(outputData);
}
/ * * *@hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
Result() {
// Restricting access to the constructor, to give Result a sealed class
// like behavior.
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final class Success extends Result {
private final Data mOutputData;
public Success() {
this(Data.EMPTY);
}
public Success(@NonNull Data outputData) {
super(a); mOutputData = outputData; }/ * * *@hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @NonNull Data getOutputData() {
return mOutputData;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null|| getClass() ! = o.getClass())return false;
Success success = (Success) o;
return mOutputData.equals(success.mOutputData);
}
@Override
public int hashCode() {
String name = Success.class.getName();
return 31 * name.hashCode() + mOutputData.hashCode();
}
@Override
public String toString() {
return "Success {" + "mOutputData=" + mOutputData + '} '; }}@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final class Failure extends Result {
private final Data mOutputData;
public Failure() {
this(Data.EMPTY);
}
public Failure(@NonNull Data outputData) {
super(a); mOutputData = outputData; }@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @NonNull Data getOutputData() {
return mOutputData;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null|| getClass() ! = o.getClass())return false;
Failure failure = (Failure) o;
return mOutputData.equals(failure.mOutputData);
}
@Override
public int hashCode() {
String name = Failure.class.getName();
return 31 * name.hashCode() + mOutputData.hashCode();
}
@Override
public String toString() {
return "Failure {" + "mOutputData=" + mOutputData + '} '; }}@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final class Retry extends Result {
public Retry() {
super(a); }@Override
public boolean equals(Object o) {
if (this == o) return true;
// We are treating all instances of Retry as equivalent.
returno ! =null && getClass() == o.getClass();
}
@Override
public int hashCode() {
String name = Retry.class.getName();
return name.hashCode();
}
@Override
public String toString() {
return "Retry"; }}}}// Task related parameters
public final class WorkerParameters {
private @NonNull UUID mId; // random sequence
private @NonNull Data mInputData;// Enter parameters
private @NonNull Set<String> mTags;The TAG / / task
private @NonNull RuntimeExtras mRuntimeExtras; // Run time parameters
private int mRunAttemptCount; // Number of retries
private @NonNull Executor mBackgroundExecutor; // Background task execution thread pool
private @NonNull TaskExecutor mWorkTaskExecutor; // Task execution thread pool
private @NonNull WorkerFactory mWorkerFactory; // Mission factory
private @NonNull ProgressUpdater mProgressUpdater;// Progress updates
private @NonNull ForegroundUpdater mForegroundUpdater;// Foreground updater
}
Copy the code
Summary:
WorkerParameters defines a series of task-related parameters, including mInputData, task identification TAG, task execution thread pool, task progress update, task factory, etc
Results: the Result/Success/Failure/Retry)
Ii. Task execution conditions (Task constraints)
We perform the task with a set of constraints, and the fields are annotated with @columnInfo, which means they are written to the database
A. Constraints on network conditions are as follows:
public enum NetworkType {
// The task has no network requirements
NOT_REQUIRED,
// You can execute tasks as long as you have a network connection
CONNECTED,
// Non-metered network links can perform tasks
UNMETERED,
// Non-roaming network connections can perform tasks
NOT_ROAMING,
// Metering network connection can perform tasks
METERED
}
// Network condition judgment
public class NetworkStateTracker extends ConstraintTracker<NetworkState> {
// Synthetic Accessor
static final String TAG = Logger.tagWithPrefix("NetworkStateTracker");
private final ConnectivityManager mConnectivityManager;
@RequiresApi(24)
private NetworkStateCallback mNetworkCallback;
private NetworkStateBroadcastReceiver mBroadcastReceiver;
public NetworkStateTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
super(context, taskExecutor);
mConnectivityManager =
(ConnectivityManager) mAppContext.getSystemService(Context.CONNECTIVITY_SERVICE);
// If NetworkCallback is supported, use NetworkCallback to listen for network connections
// Otherwise use broadcast
if (isNetworkCallbackSupported()) {
mNetworkCallback = new NetworkStateCallback();
} else {
mBroadcastReceiver = newNetworkStateBroadcastReceiver(); }}@Override
public NetworkState getInitialState(a) {
return getActiveNetworkState();
}
@Override
public void startTracking(a) {
if (isNetworkCallbackSupported()) {
try {
Logger.get().debug(TAG, "Registering network callback");
mConnectivityManager.registerDefaultNetworkCallback(mNetworkCallback);
} catch (IllegalArgumentException | SecurityException e) {
}
} else {
Logger.get().debug(TAG, "Registering broadcast receiver");
mAppContext.registerReceiver(mBroadcastReceiver,
newIntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); }}@Override
public void stopTracking(a) {
if (isNetworkCallbackSupported()) {
try {
Logger.get().debug(TAG, "Unregistering network callback");
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
} catch (IllegalArgumentException | SecurityException e) {
}
} else {
Logger.get().debug(TAG, "Unregistering broadcast receiver"); mAppContext.unregisterReceiver(mBroadcastReceiver); }}// Check whether NetworkCallback API version is supported >=24
private static boolean isNetworkCallbackSupported(a) {
// Based on requiring ConnectivityManager#registerDefaultNetworkCallback - added in API 24.
return Build.VERSION.SDK_INT >= 24;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
NetworkState getActiveNetworkState(a) {
// Use getActiveNetworkInfo() instead of getNetworkInfo(network) because it can detect VPNs.
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
booleanisConnected = info ! =null && info.isConnected();
boolean isValidated = isActiveNetworkValidated();
boolean isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager);
booleanisNotRoaming = info ! =null && !info.isRoaming();
return new NetworkState(isConnected, isValidated, isMetered, isNotRoaming);
}
// Check whether the network is valid
private boolean isActiveNetworkValidated(a) {
if (Build.VERSION.SDK_INT < 23) {
return false; // NET_CAPABILITY_VALIDATED not available until API 23. Used on API 26+.
}
Network network = mConnectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities(network);
returncapabilities ! =null
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
}
@RequiresApi(24)
private class NetworkStateCallback extends NetworkCallback {
NetworkStateCallback() {
}
@Override
public void onCapabilitiesChanged(
@NonNull Network network, @NonNull NetworkCapabilities capabilities) {
setState(getActiveNetworkState());
}
@Override
public void onLost(@NonNull Network network) { setState(getActiveNetworkState()); }}private class NetworkStateBroadcastReceiver extends BroadcastReceiver {
NetworkStateBroadcastReceiver() {
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
Logger.get().debug(TAG, "Network broadcast received"); setState(getActiveNetworkState()); }}}}Copy the code
When API>=24, NetworkStateCallback is used to monitor the change of network connection status. When API < 24, BroadcastReceiver is used to monitor system broadcast CONNECTIVITY_ACTION to monitor the change of network connection status. After the change, network metering, roaming, connection status, etc., will be updated synchronously
B. Charging constraints
public class BatteryChargingTracker extends BroadcastReceiverConstraintTracker<Boolean> {
private static final String TAG = Logger.tagWithPrefix("BatteryChrgTracker");
/**
* Create an instance of {@link BatteryChargingTracker}.
* @param context The application {@link Context}
* @param taskExecutor The internal {@link TaskExecutor} being used by WorkManager.
*/
public BatteryChargingTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
super(context, taskExecutor);
}
// Get whether the initial state is charging
@Override
public Boolean getInitialState(a) {
// {@link ACTION_CHARGING} and {@link ACTION_DISCHARGING} are not sticky broadcasts, so
// we use {@link ACTION_BATTERY_CHANGED} on all APIs to get the initial state.
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent intent = mAppContext.registerReceiver(null, intentFilter);
if (intent == null) {
Logger.get().error(TAG, "getInitialState - null intent received");
return null;
}
return isBatteryChangedIntentCharging(intent);
}
@Override
public IntentFilter getIntentFilter(a) {
IntentFilter intentFilter = new IntentFilter();
// Version 23 and above Monitor ACTION_CHARGING and ACTION_ADEQUACY
ACTION_POWER_CONNECTED and ACTION_POWER_DISCONNECTED are listened for at least version 23
if (Build.VERSION.SDK_INT >= 23) {
intentFilter.addAction(BatteryManager.ACTION_CHARGING);
intentFilter.addAction(BatteryManager.ACTION_DISCHARGING);
} else {
intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
}
return intentFilter;
}
// Monitor system broadcast is mainly charge and power
@Override
public void onBroadcastReceive(Context context, @NonNull Intent intent) {
String action = intent.getAction();
if (action == null) {
return;
}
Logger.get().debug(TAG, String.format("Received %s", action));
switch (action) {
case BatteryManager.ACTION_CHARGING:
setState(true);
break;
case BatteryManager.ACTION_DISCHARGING:
setState(false);
break;
case Intent.ACTION_POWER_CONNECTED:
setState(true);
break;
case Intent.ACTION_POWER_DISCONNECTED:
setState(false);
break; }}// Check whether the battery is charging
private boolean isBatteryChangedIntentCharging(Intent intent) {
boolean charging;
// Version 23 and above get batteryManager. EXTRA_STATUS to judge
//23 Get batteryManager. EXTRA_PLUGGED below to determine
if (Build.VERSION.SDK_INT >= 23) {
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
charging = (status == BatteryManager.BATTERY_STATUS_CHARGING
|| status == BatteryManager.BATTERY_STATUS_FULL);
} else {
int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); charging = (chargePlug ! =0);
}
returncharging; }}Copy the code
Api23 is the boundary for judging whether the device is in charging state. Following 23, charging and power-off can be judged by listening to broadcast ACTION_POWER_CONNECTED and ACTION_POWER_DISCONNECTED. 23 and above Determine the charging status of the BATTERY by monitoring ACTION_CHARGING and Action_ADEQUACY
C. Check whether the device is idle
< API>=23; < API>=23; Foreground tasks cannot set this condition; Tasks that have set setBackoffCriteria cannot use this condition
D. Device memory constraints
public class StorageNotLowTracker extends BroadcastReceiverConstraintTracker<Boolean> {
private static final String TAG = Logger.tagWithPrefix("StorageNotLowTracker");
public StorageNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
super(context, taskExecutor);
}
@Override
public Boolean getInitialState(a) {
Intent intent = mAppContext.registerReceiver(null, getIntentFilter());
if (intent == null || intent.getAction() == null) {
return true;
} else {
switch (intent.getAction()) {
case Intent.ACTION_DEVICE_STORAGE_OK:
return true;
case Intent.ACTION_DEVICE_STORAGE_LOW:
return false;
default:
return null; }}}@Override
public IntentFilter getIntentFilter(a) {
// In API 26+, DEVICE_STORAGE_OK/LOW are deprecated and are no longer sent to
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
return intentFilter;
}
@Override
public void onBroadcastReceive(Context context, @NonNull Intent intent) {
if (intent.getAction() == null) {
return; // Should never happen since the IntentFilter was configured.
}
Logger.get().debug(TAG, String.format("Received %s", intent.getAction()));
switch (intent.getAction()) {
case Intent.ACTION_DEVICE_STORAGE_OK:
setState(true);
break;
case Intent.ACTION_DEVICE_STORAGE_LOW:
setState(false);
break; }}}Copy the code
As you can see, the code logic is also very simple. It also listens to the broadcasts ACTION_DEVICE_STORAGE_OK and ACTION_DEVICE_STORAGE_LOW to determine the device storage constraint. However, in version > API26 these two broadcasts are deprecated and may not be sent in the future, so use this condition with caution
E. Electric quantity constraint
public class BatteryNotLowTracker extends BroadcastReceiverConstraintTracker<Boolean> {
private static final String TAG = Logger.tagWithPrefix("BatteryNotLowTracker");
// Low power threshold <=15%
static final float BATTERY_LOW_THRESHOLD = 0.15 f;
public BatteryNotLowTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
super(context, taskExecutor);
}
// Get the initial state
@Override
public Boolean getInitialState(a) {
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent intent = mAppContext.registerReceiver(null, intentFilter);
if (intent == null) {
Logger.get().error(TAG, "getInitialState - null intent received");
return null;
}
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float batteryPercentage = level / (float) scale;
return (status == BatteryManager.BATTERY_STATUS_UNKNOWN
|| batteryPercentage > BATTERY_LOW_THRESHOLD);
}
@Override
public IntentFilter getIntentFilter(a) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_OKAY);
intentFilter.addAction(Intent.ACTION_BATTERY_LOW);
return intentFilter;
}
// Monitor power changes
@Override
public void onBroadcastReceive(Context context, @NonNull Intent intent) {
if (intent.getAction() == null) {
return;
}
Logger.get().debug(TAG, String.format("Received %s", intent.getAction()));
switch (intent.getAction()) {
case Intent.ACTION_BATTERY_OKAY:
setState(true);
break;
case Intent.ACTION_BATTERY_LOW:
setState(false);
break; }}}Copy the code
The code is also relatively simple: whether the battery is 15% lower than the threshold. Listen to ACTION_BATTERY_OKAY and ACTION_BATTERY_LOW broadcasts to determine whether this constraint is met. The problem is that the threshold is 15% as defined. This will lead to inconsistencies, where the trigger conditions may not be consistent.
3. Task information
public final class WorkInfo {
// Task ID
private @NonNull UUID mId;
// Task status
private @NonNull State mState
// Task output data
private @NonNull Data mOutputData;
// Task identifier tag
private @NonNull Set<String> mTags;
// Task progress
private @NonNull Data mProgress;
// Number of task attempts
private int mRunAttemptCount;
// Task status
public enum State {
/ / team
ENQUEUED,
/ / implementation
RUNNING,
// The command is successfully executed
SUCCEEDED,
// Execution failed
FAILED,
// Task is blocked
BLOCKED,
// Task cancelled
CANCELLED;
// Whether the task is in the completed state (succeeded, failed, canceled)
public boolean isFinished(a) {
return (this == SUCCEEDED || this == FAILED || this== CANCELLED); }}}Copy the code
The information of a task contains a series of states and some data during the execution of the task, such as progress and status. WorkSpec is mainly the data written into the database related to the task information
Iv. Task execution
With task creation, task constraints, and task constraints, let’s look at how tasks are executed.
Single mission:
// The upper layer calls enqueue
WorkManager.getInstance(this).enqueue(workRequest)
//getInstance returns the implementation class WorkManagerImpl
public static @NonNull WorkManager getInstance(@NonNull Context context) {
return WorkManagerImpl.getInstance(context);
}
// workManager. Java converts the workRequest to a list and calls enQueue again
@NonNull
public final Operation enqueue(@NonNull WorkRequest workRequest) {
return enqueue(Collections.singletonList(workRequest));
}
//WorkManagerImpl.java
// EnQueue is the interface method and WorkManagerImpl is the implementation of WorkManager
@Override
@NonNull
public Operation enqueue(@NonNull List<? extends WorkRequest> workRequests) {
// Check whether the input parameter is empty
if (workRequests.isEmpty()) {
throw new IllegalArgumentException(
"enqueue needs at least one WorkRequest.");
}
// New WorkContinuationImpl, then call enQueue of WorkContinuationImpl
return new WorkContinuationImpl(this, workRequests).enqueue();
}
//WorkContinuationImpl.java
WorkContinuationImpl(
@NonNull WorkManagerImpl workManagerImpl,
@NonNull List<? extends WorkRequest> work) {
this(
workManagerImpl,
null,
ExistingWorkPolicy.KEEP,
work,
null);
}
/ / equeue method
@Override
public @NonNull Operation enqueue(a) {
// Check whether the queue is in
if(! mEnqueued) {// Wrap the runabl as runable and execute the runabl in the thread pool. Runable holds WorkContinuationImpl
EnqueueRunnable runnable = new EnqueueRunnable(this);
mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
mOperation = runnable.getOperation();
} else {
Logger.get().warning(TAG,
String.format("Already enqueued work ids (%s)", TextUtils.join(",", mIds)));
}
return mOperation;
}
// The above method calls the thread pool to execute runable, so let's look at runable's run method
@Override
public void run(a) {
try {
// Check if there is a loop
if (mWorkContinuation.hasCycles()) {
throw new IllegalStateException(
String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
}
// Add to database
boolean needsScheduling = addToDatabase();
// If true, start RescheduleReceiver and call scheduleWorkInBackground()
if (needsScheduling) {
// Enable RescheduleReceiver, only when there are Worker's that need scheduling.
final Context context =
mWorkContinuation.getWorkManagerImpl().getApplicationContext();
PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true); // Background execution
scheduleWorkInBackground();
}
mOperation.setState(Operation.SUCCESS);
} catch (Throwable exception) {
mOperation.setState(newOperation.State.FAILURE(exception)); }}// Let's start with addToDatabase
// Get the database and call processContinuation
@VisibleForTesting
public boolean addToDatabase(a) {
WorkManagerImpl workManagerImpl = mWorkContinuation.getWorkManagerImpl();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
workDatabase.beginTransaction();
try {
boolean needsScheduling = processContinuation(mWorkContinuation);
workDatabase.setTransactionSuccessful();
return needsScheduling;
} finally{ workDatabase.endTransaction(); }}private static boolean processContinuation(@NonNull WorkContinuationImpl workContinuation) {
boolean needsScheduling = false;
List<WorkContinuationImpl> parents = workContinuation.getParents();
if(parents ! =null) {
for (WorkContinuationImpl parent : parents) {
if(! parent.isEnqueued()) { needsScheduling |= processContinuation(parent); }else {
Logger.get().warning(TAG, String.format("Already enqueued work ids (%s).",
TextUtils.join(",", parent.getIds()))); }}}// Add parents to empty, enqueueContinuation is called
needsScheduling |= enqueueContinuation(workContinuation);
return needsScheduling;
}
//
private static boolean enqueueContinuation(@NonNull WorkContinuationImpl workContinuation) {
Set<String> prerequisiteIds = WorkContinuationImpl.prerequisitesFor(workContinuation);
boolean needsScheduling = enqueueWorkWithPrerequisites(
workContinuation.getWorkManagerImpl(),
workContinuation.getWork(),
prerequisiteIds.toArray(new String[0]),
workContinuation.getName(),
workContinuation.getExistingWorkPolicy());
workContinuation.markEnqueued();
return needsScheduling;
}
private static boolean enqueueWorkWithPrerequisites(
WorkManagerImpl workManagerImpl,
@NonNull List<? extends WorkRequest> workList,
String[] prerequisiteIds,
String name,
ExistingWorkPolicy existingWorkPolicy) {
boolean needsScheduling = false;
long currentTimeMillis = System.currentTimeMillis();
WorkDatabase workDatabase = workManagerImpl.getWorkDatabase();
booleanhasPrerequisite = (prerequisiteIds ! =null && prerequisiteIds.length > 0);
boolean hasCompletedAllPrerequisites = true;
boolean hasFailedPrerequisites = false;
boolean hasCancelledPrerequisites = false;
if (hasPrerequisite) {
for (String id : prerequisiteIds) {
WorkSpec prerequisiteWorkSpec = workDatabase.workSpecDao().getWorkSpec(id);
if (prerequisiteWorkSpec == null) {
Logger.get().error(TAG,
String.format("Prerequisite %s doesn't exist; not enqueuing", id));
return false;
}
WorkInfo.State prerequisiteState = prerequisiteWorkSpec.state;
hasCompletedAllPrerequisites &= (prerequisiteState == SUCCEEDED);
if (prerequisiteState == FAILED) {
hasFailedPrerequisites = true;
} else if (prerequisiteState == CANCELLED) {
hasCancelledPrerequisites = true; }}}booleanisNamed = ! TextUtils.isEmpty(name);booleanshouldApplyExistingWorkPolicy = isNamed && ! hasPrerequisite;if (shouldApplyExistingWorkPolicy) {
List<WorkSpec.IdAndState> existingWorkSpecIdAndStates =
workDatabase.workSpecDao().getWorkSpecIdAndStatesForName(name);
if(! existingWorkSpecIdAndStates.isEmpty()) {// Policy processing
if (existingWorkPolicy == APPEND || existingWorkPolicy == APPEND_OR_REPLACE) {
DependencyDao dependencyDao = workDatabase.dependencyDao();
List<String> newPrerequisiteIds = new ArrayList<>();
for (WorkSpec.IdAndState idAndState : existingWorkSpecIdAndStates) {
if(! dependencyDao.hasDependents(idAndState.id)) { hasCompletedAllPrerequisites &= (idAndState.state == SUCCEEDED);if (idAndState.state == FAILED) {
hasFailedPrerequisites = true;
} else if (idAndState.state == CANCELLED) {
hasCancelledPrerequisites = true; } newPrerequisiteIds.add(idAndState.id); }}if (existingWorkPolicy == APPEND_OR_REPLACE) {
if (hasCancelledPrerequisites || hasFailedPrerequisites) {
// Delete all WorkSpecs with this name
WorkSpecDao workSpecDao = workDatabase.workSpecDao();
List<WorkSpec.IdAndState> idAndStates =
workSpecDao.getWorkSpecIdAndStatesForName(name);
for (WorkSpec.IdAndState idAndState : idAndStates) {
workSpecDao.delete(idAndState.id);
}
// Treat this as a new chain of work.
newPrerequisiteIds = Collections.emptyList();
hasCancelledPrerequisites = false;
hasFailedPrerequisites = false;
}
}
prerequisiteIds = newPrerequisiteIds.toArray(prerequisiteIds);
hasPrerequisite = (prerequisiteIds.length > 0);
} else {
if (existingWorkPolicy == KEEP) {
for (WorkSpec.IdAndState idAndState : existingWorkSpecIdAndStates) {
if (idAndState.state == ENQUEUED || idAndState.state == RUNNING) {
return false;
}
}
}
CancelWorkRunnable.forName(name, workManagerImpl, false).run();
needsScheduling = true;
WorkSpecDao workSpecDao = workDatabase.workSpecDao();
for(WorkSpec.IdAndState idAndState : existingWorkSpecIdAndStates) { workSpecDao.delete(idAndState.id); }}}}/ / traverse workRequest
for (WorkRequest work : workList) {
// Get the work parameter
WorkSpec workSpec = work.getWorkSpec();
if(hasPrerequisite && ! hasCompletedAllPrerequisites) {if (hasFailedPrerequisites) {
workSpec.state = FAILED;
} else if (hasCancelledPrerequisites) {
workSpec.state = CANCELLED;
} else{ workSpec.state = BLOCKED; }}else {
// If it is a periodic task, record the current start time
if(! workSpec.isPeriodic()) { workSpec.periodStartTime = currentTimeMillis; }else {
// Otherwise, the start time is 0
workSpec.periodStartTime = 0L; }}//API 23 =< <=25 tryDelegateConstrainedWorkSpec
/ / API < = 22 and usesScheduler also call tryDelegateConstrainedWorkSpec
if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL
&& Build.VERSION.SDK_INT <= 25) {
tryDelegateConstrainedWorkSpec(workSpec);
} else if (Build.VERSION.SDK_INT <= WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL
&& usesScheduler(workManagerImpl, Schedulers.GCM_SCHEDULER)) {
tryDelegateConstrainedWorkSpec(workSpec);
}
if (workSpec.state == ENQUEUED) {
needsScheduling = true;
}
// Add to database
workDatabase.workSpecDao().insertWorkSpec(workSpec);
if (hasPrerequisite) {
for (String prerequisiteId : prerequisiteIds) {
Dependency dep = newDependency(work.getStringId(), prerequisiteId); workDatabase.dependencyDao().insertDependency(dep); }}for (String tag : work.getTags()) {
workDatabase.workTagDao().insert(new WorkTag(tag, work.getStringId()));
}
if (isNamed) {
workDatabase.workNameDao().insert(newWorkName(name, work.getStringId())); }}return needsScheduling;
}
// Power-dependent and space-dependent versions 23-25 ConstraintTrackingWorker and above 26 rely on JobScheduler
private static void tryDelegateConstrainedWorkSpec(WorkSpec workSpec) {
// requiresBatteryNotLow and requiresStorageNotLow require API 26 for JobScheduler.
// Delegate to ConstraintTrackingWorker between API 23-25.
Constraints constraints = workSpec.constraints;
if (constraints.requiresBatteryNotLow() || constraints.requiresStorageNotLow()) {
String workerClassName = workSpec.workerClassName;
Data.Builder builder = new Data.Builder();
// Copy all argumentsbuilder.putAll(workSpec.input) .putString(ARGUMENT_CLASS_NAME, workerClassName); workSpec.workerClassName = ConstraintTrackingWorker.class.getName(); workSpec.input = builder.build(); }}/** * Schedules work on the background scheduler. */
@VisibleForTesting
public void scheduleWorkInBackground(a) {
WorkManagerImpl workManager = mWorkContinuation.getWorkManagerImpl();
Schedulers.schedule(
workManager.getConfiguration(),
workManager.getWorkDatabase(),
workManager.getSchedulers());
}
Copy the code
Finally, schedulers.schedule is dialed, passing in the configuration, database, and Scheluer list
Let’s first look at the schedulers list, which is an argument to the workManagerImpl
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public WorkManagerImpl(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor workTaskExecutor,
@NonNull WorkDatabase database) {
Context applicationContext = context.getApplicationContext();
Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
Schedulers are generated on createSchedulers
List<Scheduler> schedulers =
createSchedulers(applicationContext, configuration, workTaskExecutor);
Processor processor = new Processor(
context,
configuration,
workTaskExecutor,
database,
schedulers);
internalInit(context, configuration, workTaskExecutor, database, schedulers, processor);
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public List<Scheduler> createSchedulers(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor taskExecutor) {
/ / the first list item is createBestAvailableBackgroundScheduler
// The second list item is GreedyScheduler
return Arrays.asList(
Schedulers.createBestAvailableBackgroundScheduler(context, this),
new GreedyScheduler(context, configuration, taskExecutor, this));
}
//Shedulers.java
@NonNull
static Scheduler createBestAvailableBackgroundScheduler(
@NonNull Context context,
@NonNull WorkManagerImpl workManager) {
Scheduler scheduler;
//23 and above use SystemJobScheduler and depend on SystemJobService
if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
scheduler = new SystemJobScheduler(context, workManager);
setComponentEnabled(context, SystemJobService.class, true);
Logger.get().debug(TAG, "Created SystemJobScheduler and enabled SystemJobService");
} else {
//23 Reflection is used to obtain the GcmScheduler. If not, SystemAlarmScheduler is used and SystemAlarmService is used
scheduler = tryCreateGcmBasedScheduler(context);
if (scheduler == null) {
scheduler = new SystemAlarmScheduler(context);
setComponentEnabled(context, SystemAlarmService.class, true);
Logger.get().debug(TAG, "Created SystemAlarmScheduler"); }}return scheduler;
}
@Nullable
private static Scheduler tryCreateGcmBasedScheduler(@NonNull Context context) {
try{ Class<? > klass = Class.forName(GCM_SCHEDULER); Scheduler scheduler = (Scheduler) klass.getConstructor(Context.class).newInstance(context); Logger.get().debug(TAG, String.format("Created %s", GCM_SCHEDULER));
return scheduler;
} catch (Throwable throwable) {
Logger.get().debug(TAG, "Unable to create GCM Scheduler", throwable);
return null; }}Copy the code
How does the constraint work? We know that the last call is sheduler. shedule. Let’s look at this method:
@Override
public void schedule(@NonNull WorkSpec... workSpecs) {
if (mIsMainProcess == null) {
mIsMainProcess = TextUtils.equals(mContext.getPackageName(), getProcessName());
}
if(! mIsMainProcess) { Logger.get().info(TAG,"Ignoring schedule request in non-main process");
return;
}
registerExecutionListenerIfNeeded();
// A constrained set of work parameters
Set<WorkSpec> constrainedWorkSpecs = new HashSet<>();
// A constrained set of workids
Set<String> constrainedWorkSpecIds = new HashSet<>();
for (WorkSpec workSpec : workSpecs) {
long nextRunTime = workSpec.calculateNextRunTime();
long now = System.currentTimeMillis();
if (workSpec.state == WorkInfo.State.ENQUEUED) {
if (now < nextRunTime) {
// Future work
if(mDelayedWorkTracker ! =null) { mDelayedWorkTracker.schedule(workSpec); }}else if (workSpec.hasConstraints()) {
if (SDK_INT >= 23 && workSpec.constraints.requiresDeviceIdle()) {
// The constraint that the device is idle is ignored above
Logger.get().debug(TAG,
String.format("Ignoring WorkSpec %s, Requires device idle.",
workSpec));
} else if (SDK_INT >= 24 && workSpec.constraints.hasContentUriTriggers()) {
// The contentUri constraint is ignored above
Logger.get().debug(TAG,
String.format("Ignoring WorkSpec %s, Requires ContentUri triggers.",
workSpec));
} else {
// Add the constraint to the collection
constrainedWorkSpecs.add(workSpec);
// Add the task ID to the collectionconstrainedWorkSpecIds.add(workSpec.id); }}else {
// There is no constraint to call startWork directly
Logger.get().debug(TAG, String.format("Starting work for %s", workSpec.id)); mWorkManagerImpl.startWork(workSpec.id); }}}// onExecuted() which is called on the main thread also modifies the list of mConstrained
// WorkSpecs. Therefore we need to lock here.
synchronized (mLock) {
if(! constrainedWorkSpecs.isEmpty()) { Logger.get().debug(TAG, String.format("Starting tracking for [%s]",
TextUtils.join(",", constrainedWorkSpecIds))); mConstrainedWorkSpecs.addAll(constrainedWorkSpecs); mWorkConstraintsTracker.replace(mConstrainedWorkSpecs); }}}Copy the code
Startwork is not executed if there is a constraint. Let’s assume there is no constraint. Look at startWork
/ * * *@param workSpecId The {@link WorkSpec} id to start
* @hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void startWork(@NonNull String workSpecId) {
startWork(workSpecId, null);
}
/ * * *@param workSpecId The {@link WorkSpec} id to start
* @param runtimeExtras The {@link WorkerParameters.RuntimeExtras} associated with this work
* @hide* /
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void startWork(
@NonNull String workSpecId,
@Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
// Call the background thread to execute the encapsulated runable
mWorkTaskExecutor
.executeOnBackgroundThread(
new StartWorkRunnable(this, workSpecId, runtimeExtras));
}
// startWorkRunnable. Java calls getProcessor.startwork
@Override
public void run(a) {
mWorkManagerImpl.getProcessor().startWork(mWorkSpecId, mRuntimeExtras);
}
//Processor.java wraps workWrapper and calls the thread pool for execution
public boolean startWork(
@NonNull String id,
@Nullable WorkerParameters.RuntimeExtras runtimeExtras) {
WorkerWrapper workWrapper;
synchronized (mLock) {
// Work may get triggered multiple times if they have passing constraints
// and new work with those constraints are added.
if (isEnqueued(id)) {
Logger.get().debug(
TAG,
String.format("Work %s is already enqueued for processing", id));
return false;
}
workWrapper =
new WorkerWrapper.Builder(
mAppContext,
mConfiguration,
mWorkTaskExecutor,
this,
mWorkDatabase,
id)
.withSchedulers(mSchedulers)
.withRuntimeExtras(runtimeExtras)
.build();
ListenableFuture<Boolean> future = workWrapper.getFuture();
future.addListener(
new FutureListener(this, id, future),
mWorkTaskExecutor.getMainThreadExecutor());
mEnqueuedWorkMap.put(id, workWrapper);
}
mWorkTaskExecutor.getBackgroundExecutor().execute(workWrapper);
Logger.get().debug(TAG, String.format("%s: processing %s", getClass().getSimpleName(), id));
return true;
}
/ / WorkRapper. Java run method
@WorkerThread
@Override
public void run(a) {
mTags = mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId);
mWorkDescription = createWorkDescription(mTags);
/ / call runworker
runWorker();
}
private void runWorker(a) {
if (tryCheckForInterruptionAndResolve()) {
return;
}
mWorkDatabase.beginTransaction();
try {
// Get task parameters
mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
if (mWorkSpec == null) {
Logger.get().error(
TAG,
String.format("Didn't find WorkSpec for id %s", mWorkSpecId));
resolve(false);
return;
}
if(mWorkSpec.state ! = ENQUEUED) { resolveIncorrectStatus(); mWorkDatabase.setTransactionSuccessful(); Logger.get().debug(TAG, String.format("%s is not in ENQUEUED state. Nothing more to do.",
mWorkSpec.workerClassName));
return;
}
// Periodic task and delayed task processing
if (mWorkSpec.isPeriodic() || mWorkSpec.isBackedOff()) {
long now = System.currentTimeMillis();
// Is not the first time to execute the task and calculate the next time to execute the task
boolean isFirstRun = mWorkSpec.periodStartTime == 0;
if(! isFirstRun && now < mWorkSpec.calculateNextRunTime()) { Logger.get().debug(TAG, String.format("Delaying execution for %s because it is being executed "
+ "before schedule.",
mWorkSpec.workerClassName));
resolve(true);
return;
}
}
mWorkDatabase.setTransactionSuccessful();
} finally {
mWorkDatabase.endTransaction();
}
// Handle the parameters carried
Data input;
if (mWorkSpec.isPeriodic()) {
input = mWorkSpec.input;
} else {
InputMergerFactory inputMergerFactory = mConfiguration.getInputMergerFactory();
String inputMergerClassName = mWorkSpec.inputMergerClassName;
InputMerger inputMerger =
inputMergerFactory.createInputMergerWithDefaultFallback(inputMergerClassName);
if (inputMerger == null) {
Logger.get().error(TAG, String.format("Could not create Input Merger %s",
mWorkSpec.inputMergerClassName));
setFailedAndResolve();
return;
}
List<Data> inputs = new ArrayList<>();
inputs.add(mWorkSpec.input);
inputs.addAll(mWorkSpecDao.getInputsFromPrerequisites(mWorkSpecId));
input = inputMerger.merge(inputs);
}
WorkerParameters params = new WorkerParameters(
UUID.fromString(mWorkSpecId),
input,
mTags,
mRuntimeExtras,
mWorkSpec.runAttemptCount,
mConfiguration.getExecutor(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
new WorkProgressUpdater(mWorkDatabase, mWorkTaskExecutor),
new WorkForegroundUpdater(mWorkDatabase, mForegroundProcessor, mWorkTaskExecutor));
// Check whether it is empty or in use before executing the task
if (mWorker == null) {
mWorker = mConfiguration.getWorkerFactory().createWorkerWithDefaultFallback(
mAppContext,
mWorkSpec.workerClassName,
params);
}
if (mWorker == null) {
Logger.get().error(TAG,
String.format("Could not create Worker %s", mWorkSpec.workerClassName));
setFailedAndResolve();
return;
}
if (mWorker.isUsed()) {
Logger.get().error(TAG,
String.format("Received an already-used Worker %s; WorkerFactory should return "
+ "new instances",
mWorkSpec.workerClassName));
setFailedAndResolve();
return;
}
mWorker.setUsed();
// Try to run the task
if (trySetRunning()) {
if (tryCheckForInterruptionAndResolve()) {
return;
}
final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
// Call the startWork method on the main thread
mWorkTaskExecutor.getMainThreadExecutor()
.execute(new Runnable() {
@Override
public void run(a) {
try {
Logger.get().debug(TAG, String.format("Starting work for %s",
mWorkSpec.workerClassName));
mInnerFuture = mWorker.startWork();
future.setFuture(mInnerFuture);
} catch(Throwable e) { future.setException(e); }}});// Run result listener
final String workDescription = mWorkDescription;
future.addListener(new Runnable() {
@Override
@SuppressLint("SyntheticAccessor")
public void run(a) {
try {
ListenableWorker.Result result = future.get();
if (result == null) {
Logger.get().error(TAG, String.format(
"%s returned a null result. Treating it as a failure.",
mWorkSpec.workerClassName));
} else {
Logger.get().debug(TAG, String.format("%s returned a %s result.", mWorkSpec.workerClassName, result)); mResult = result; }}catch (CancellationException exception) {
// Cancellations need to be treated with care here because innerFuture
// cancellations will bubble up, and we need to gracefully handle that.
Logger.get().info(TAG, String.format("%s was cancelled", workDescription),
exception);
} catch (InterruptedException | ExecutionException exception) {
Logger.get().error(TAG,
String.format("%s failed because it threw an exception/error",
workDescription), exception);
} finally {
onWorkFinished();
}
}
}, mWorkTaskExecutor.getBackgroundExecutor());
} else{ resolveIncorrectStatus(); }}Copy the code
How do task constraints work?
We have seen above that GreedyScheduler does not call startWork under constraints. When will startwork be called
// Implement the WorkConstraintsCallback interface
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class GreedyScheduler implements Scheduler.WorkConstraintsCallback{
// Total processing of constraints
private final WorkConstraintsTracker mWorkConstraintsTracker;
// The constraint condition
private final Set<WorkSpec> mConstrainedWorkSpecs = new HashSet<>();
public GreedyScheduler(
@NonNull Context context,
@NonNull Configuration configuration,
@NonNull TaskExecutor taskExecutor,
@NonNull WorkManagerImpl workManagerImpl) {
// the constructor initializes the constraint total processing
mWorkConstraintsTracker = new WorkConstraintsTracker(context, taskExecutor, this);
}
// A callback that implements all constraints of the interface invokes Stratwork
@Override
public void onAllConstraintsMet(@NonNull List<String> workSpecIds) {
for (String workSpecId : workSpecIds) {
Logger.get().debug(
TAG,
String.format("Constraints met: Scheduling work ID %s", workSpecId)); mWorkManagerImpl.startWork(workSpecId); }}// Callbacks that implement all constraints of the interface that do not meet will call stopWork
@Override
public void onAllConstraintsNotMet(@NonNull List<String> workSpecIds) {
for (String workSpecId : workSpecIds) {
Logger.get().debug(TAG,
String.format("Constraints not met: Cancelling work ID %s", workSpecId)); mWorkManagerImpl.stopWork(workSpecId); }}}Copy the code
GreedyScheduler implements the WorkConstraintsCallback interface, which calls onAllConstraintsMet and startWork when all constraints are met