RemoteListenableWorker.java
/*
* Copyright 2021 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.multiprocess;
import android.content.ComponentName;
import android.content.Context;
import android.os.RemoteException;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import androidx.work.Logger;
import androidx.work.WorkerParameters;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.futures.SettableFuture;
import androidx.work.multiprocess.parcelable.ParcelConverters;
import androidx.work.multiprocess.parcelable.ParcelableRemoteWorkRequest;
import androidx.work.multiprocess.parcelable.ParcelableResult;
import androidx.work.multiprocess.parcelable.ParcelableWorkerParameters;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.Executor;
/**
* Is an implementation of a {@link ListenableWorker} that can bind to a remote process.
* <p>
* To be able to bind to a remote process, A {@link RemoteListenableWorker} needs additional
* arguments as part of its input {@link Data}.
* <p>
* The arguments ({@link #ARGUMENT_PACKAGE_NAME}, {@link #ARGUMENT_CLASS_NAME}) are used to
* determine the {@link android.app.Service} that the {@link RemoteListenableWorker} can bind to.
* {@link #startRemoteWork()} is then subsequently called in the process that the
* {@link android.app.Service} is running in.
*/
public abstract class RemoteListenableWorker extends ListenableWorker {
// Synthetic access
static final String TAG = Logger.tagWithPrefix("RemoteListenableWorker");
/**
* The {@code #ARGUMENT_PACKAGE_NAME}, {@link #ARGUMENT_CLASS_NAME} together determine the
* {@link ComponentName} that the {@link RemoteListenableWorker} binds to before calling
* {@link #startRemoteWork()}.
*/
public static final String ARGUMENT_PACKAGE_NAME =
"androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
/**
* The {@link #ARGUMENT_PACKAGE_NAME}, {@code className} together determine the
* {@link ComponentName} that the {@link RemoteListenableWorker} binds to before calling
* {@link #startRemoteWork()}.
*/
public static final String ARGUMENT_CLASS_NAME =
"androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
// Synthetic access
final WorkerParameters mWorkerParameters;
// Synthetic access
final WorkManagerImpl mWorkManager;
// Synthetic access
final Executor mExecutor;
// Synthetic access
final ListenableWorkerImplClient mClient;
// Synthetic access
@Nullable
String mWorkerClassName;
@Nullable
private ComponentName mComponentName;
/**
* @param appContext The application {@link Context}
* @param workerParams {@link WorkerParameters} to setup the internal state of this worker
*/
public RemoteListenableWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
mWorkerParameters = workerParams;
mWorkManager = WorkManagerImpl.getInstance(appContext);
mExecutor = mWorkManager.getWorkTaskExecutor().getBackgroundExecutor();
mClient = new ListenableWorkerImplClient(getApplicationContext(), mExecutor);
}
@Override
@NonNull
public final ListenableFuture<Result> startWork() {
SettableFuture<Result> future = SettableFuture.create();
Data data = getInputData();
final String id = mWorkerParameters.getId().toString();
String packageName = data.getString(ARGUMENT_PACKAGE_NAME);
String serviceClassName = data.getString(ARGUMENT_CLASS_NAME);
if (TextUtils.isEmpty(packageName)) {
String message = "Need to specify a package name for the Remote Service.";
Logger.get().error(TAG, message);
future.setException(new IllegalArgumentException(message));
return future;
}
if (TextUtils.isEmpty(serviceClassName)) {
String message = "Need to specify a class name for the Remote Service.";
Logger.get().error(TAG, message);
future.setException(new IllegalArgumentException(message));
return future;
}
mComponentName = new ComponentName(packageName, serviceClassName);
ListenableFuture<byte[]> result = mClient.execute(
mComponentName,
new RemoteDispatcher<IListenableWorkerImpl>() {
@Override
public void execute(
@NonNull IListenableWorkerImpl listenableWorkerImpl,
@NonNull IWorkManagerImplCallback callback) throws RemoteException {
WorkSpec workSpec = mWorkManager.getWorkDatabase()
.workSpecDao()
.getWorkSpec(id);
mWorkerClassName = workSpec.workerClassName;
ParcelableRemoteWorkRequest remoteWorkRequest =
new ParcelableRemoteWorkRequest(
workSpec.workerClassName, mWorkerParameters
);
byte[] request = ParcelConverters.marshall(remoteWorkRequest);
listenableWorkerImpl.startWork(request, callback);
}
});
return RemoteClientUtils.map(result, new Function<byte[], Result>() {
@Override
public Result apply(byte[] input) {
ParcelableResult parcelableResult = ParcelConverters.unmarshall(input,
ParcelableResult.CREATOR);
Logger.get().debug(TAG, "Cleaning up");
mClient.unbindService();
return parcelableResult.getResult();
}
}, mExecutor);
}
/**
* Override this method to define the work that needs to run in the remote process. This method
* is called on the main thread.
* <p>
* A ListenableWorker has a well defined
* <a href="https://d.android.com/reference/android/app/job/JobScheduler">execution window</a>
* to to finish its execution and return a {@link androidx.work.ListenableWorker.Result}.
* After this time has expired, the worker will be signalled to stop and its
* {@link ListenableFuture} will be cancelled. Note that the execution window also includes
* the cost of binding to the remote process.
* <p>
* The {@link RemoteListenableWorker} will also be signalled to stop when its constraints are
* no longer met.
*
* @return A {@link ListenableFuture} with the {@code Result} of the computation. If you
* cancel this Future, WorkManager will treat this unit of work as a {@code Result#failure()}.
*/
@NonNull
public abstract ListenableFuture<Result> startRemoteWork();
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("FutureReturnValueIgnored")
public void onStopped() {
super.onStopped();
// Delegate interruptions to the remote process.
if (mComponentName != null) {
mClient.execute(mComponentName,
new RemoteDispatcher<IListenableWorkerImpl>() {
@Override
public void execute(
@NonNull IListenableWorkerImpl listenableWorkerImpl,
@NonNull IWorkManagerImplCallback callback)
throws RemoteException {
ParcelableWorkerParameters parcelableWorkerParameters =
new ParcelableWorkerParameters(mWorkerParameters);
byte[] request = ParcelConverters.marshall(parcelableWorkerParameters);
listenableWorkerImpl.interrupt(request, callback);
}
});
}
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public ListenableFuture<Void> setProgressAsync(@NonNull Data data) {
// Delegate progress updates to the designated process.
RemoteWorkManager remoteWorkManager =
RemoteWorkManager.getInstance(getApplicationContext());
return remoteWorkManager.setProgress(getId(), data);
}
}