script-astra/Android/Sdk/sources/android-35/android/adservices/ondevicepersonalization/IsolatedService.java

561 lines
28 KiB
Java
Raw Normal View History

2025-01-20 15:15:20 +00:00
/*
* Copyright (C) 2022 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 android.adservices.ondevicepersonalization;
import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
import android.adservices.ondevicepersonalization.aidl.IFederatedComputeService;
import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
import android.adservices.ondevicepersonalization.aidl.IIsolatedService;
import android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.OutcomeReceiver;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import com.android.adservices.ondevicepersonalization.flags.Flags;
import com.android.ondevicepersonalization.internal.util.LoggerFactory;
import com.android.ondevicepersonalization.internal.util.OdpParceledListSlice;
import java.util.Objects;
import java.util.function.Function;
// TODO(b/289102463): Add a link to the public ODP developer documentation.
/**
* Base class for services that are started by ODP on a call to
* {@code OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle,
* java.util.concurrent.Executor, OutcomeReceiver)}
* and run in an <a
* href="https://developer.android.com/guide/topics/manifest/service-element#isolated">isolated
* process</a>. The service can produce content to be displayed in a
* {@link android.view.SurfaceView} in a calling app and write persistent results to on-device
* storage, which can be consumed by Federated Analytics for cross-device statistical analysis or
* by Federated Learning for model training.
* Client apps use {@link OnDevicePersonalizationManager} to interact with an {@link
* IsolatedService}.
*/
@FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
public abstract class IsolatedService extends Service {
private static final String TAG = IsolatedService.class.getSimpleName();
private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
private IBinder mBinder;
/** Creates a binder for an {@link IsolatedService}. */
@Override
public void onCreate() {
mBinder = new ServiceBinder();
}
/**
* Handles binding to the {@link IsolatedService}.
*
* @param intent The Intent that was used to bind to this service, as given to {@link
* android.content.Context#bindService Context.bindService}. Note that any extras that were
* included with the Intent at that point will <em>not</em> be seen here.
*/
@Override
@Nullable
public IBinder onBind(@NonNull Intent intent) {
return mBinder;
}
/**
* Return an instance of an {@link IsolatedWorker} that handles client requests.
*
* @param requestToken an opaque token that identifies the current request to the service that
* must be passed to service methods that depend on per-request state.
*/
@NonNull
public abstract IsolatedWorker onRequest(@NonNull RequestToken requestToken);
/**
* Returns a Data Access Object for the REMOTE_DATA table. The REMOTE_DATA table is a read-only
* key-value store that contains data that is periodically downloaded from an endpoint declared
* in the <download> tag in the ODP manifest of the service, as shown in the following example.
*
* <pre>{@code
* <!-- Contents of res/xml/OdpSettings.xml -->
* <on-device-personalization>
* <!-- Name of the service subclass -->
* <service "com.example.odpsample.SampleService">
* <!-- If this tag is present, ODP will periodically poll this URL and
* download content to populate REMOTE_DATA. Adopters that do not need to
* download content from their servers can skip this tag. -->
* <download-settings url="https://example.com/get" />
* </service>
* </on-device-personalization>
* }</pre>
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return A {@link KeyValueStore} object that provides access to the REMOTE_DATA table. The
* methods in the returned {@link KeyValueStore} are blocking operations and should be
* called from a worker thread and not the main thread or a binder thread.
* @see #onRequest(RequestToken)
*/
@NonNull
public final KeyValueStore getRemoteData(@NonNull RequestToken requestToken) {
return new RemoteDataImpl(requestToken.getDataAccessService());
}
/**
* Returns a Data Access Object for the LOCAL_DATA table. The LOCAL_DATA table is a persistent
* key-value store that the service can use to store any data. The contents of this table are
* visible only to the service running in an isolated process and cannot be sent outside the
* device.
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return A {@link MutableKeyValueStore} object that provides access to the LOCAL_DATA table.
* The methods in the returned {@link MutableKeyValueStore} are blocking operations and
* should be called from a worker thread and not the main thread or a binder thread.
* @see #onRequest(RequestToken)
*/
@NonNull
public final MutableKeyValueStore getLocalData(@NonNull RequestToken requestToken) {
return new LocalDataImpl(requestToken.getDataAccessService());
}
/**
* Returns a DAO for the REQUESTS and EVENTS tables that provides
* access to the rows that are readable by the IsolatedService.
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return A {@link LogReader} object that provides access to the REQUESTS and EVENTS table.
* The methods in the returned {@link LogReader} are blocking operations and
* should be called from a worker thread and not the main thread or a binder thread.
* @see #onRequest(RequestToken)
*/
@NonNull
public final LogReader getLogReader(@NonNull RequestToken requestToken) {
return new LogReader(requestToken.getDataAccessService());
}
/**
* Returns an {@link EventUrlProvider} for the current request. The {@link EventUrlProvider}
* provides URLs that can be embedded in HTML. When the HTML is rendered in an
* {@link android.webkit.WebView}, the platform intercepts requests to these URLs and invokes
* {@code IsolatedWorker#onEvent(EventInput, Consumer)}.
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return An {@link EventUrlProvider} that returns event tracking URLs.
* @see #onRequest(RequestToken)
*/
@NonNull
public final EventUrlProvider getEventUrlProvider(@NonNull RequestToken requestToken) {
return new EventUrlProvider(requestToken.getDataAccessService());
}
/**
* Returns the platform-provided {@link UserData} for the current request.
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return A {@link UserData} object.
* @see #onRequest(RequestToken)
*/
@Nullable
public final UserData getUserData(@NonNull RequestToken requestToken) {
return requestToken.getUserData();
}
/**
* Returns an {@link FederatedComputeScheduler} for the current request. The {@link
* FederatedComputeScheduler} can be used to schedule and cancel federated computation jobs.
* The federated computation includes federated learning and federated analytic jobs.
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return An {@link FederatedComputeScheduler} that returns a federated computation job
* scheduler.
* @see #onRequest(RequestToken)
*/
@NonNull
public final FederatedComputeScheduler getFederatedComputeScheduler(
@NonNull RequestToken requestToken) {
return new FederatedComputeScheduler(
requestToken.getFederatedComputeService(),
requestToken.getDataAccessService());
}
/**
* Returns an {@link ModelManager} for the current request. The {@link ModelManager} can be used
* to do model inference. It only supports Tensorflow Lite model inference now.
*
* @param requestToken an opaque token that identifies the current request to the service.
* @return An {@link ModelManager} that can be used for model inference.
*/
@NonNull
public final ModelManager getModelManager(@NonNull RequestToken requestToken) {
return new ModelManager(
requestToken.getDataAccessService(), requestToken.getModelService());
}
// TODO(b/228200518): Add onBidRequest()/onBidResponse() methods.
class ServiceBinder extends IIsolatedService.Stub {
@Override
public void onRequest(
int operationCode,
@NonNull Bundle params,
@NonNull IIsolatedServiceCallback resultCallback) {
Objects.requireNonNull(params);
Objects.requireNonNull(resultCallback);
final long token = Binder.clearCallingIdentity();
// TODO(b/228200518): Ensure that caller is ODP Service.
// TODO(b/323592348): Add model inference in other flows.
try {
performRequest(operationCode, params, resultCallback);
} finally {
Binder.restoreCallingIdentity(token);
}
}
private void performRequest(
int operationCode,
@NonNull Bundle params,
@NonNull IIsolatedServiceCallback resultCallback) {
if (operationCode == Constants.OP_EXECUTE) {
performExecute(params, resultCallback);
} else if (operationCode == Constants.OP_DOWNLOAD) {
performDownload(params, resultCallback);
} else if (operationCode == Constants.OP_RENDER) {
performRender(params, resultCallback);
} else if (operationCode == Constants.OP_WEB_VIEW_EVENT) {
performOnWebViewEvent(params, resultCallback);
} else if (operationCode == Constants.OP_TRAINING_EXAMPLE) {
performOnTrainingExample(params, resultCallback);
} else if (operationCode == Constants.OP_WEB_TRIGGER) {
performOnWebTrigger(params, resultCallback);
} else {
throw new IllegalArgumentException("Invalid op code: " + operationCode);
}
}
private void performOnWebTrigger(
@NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
try {
WebTriggerInputParcel inputParcel =
Objects.requireNonNull(
params.getParcelable(
Constants.EXTRA_INPUT, WebTriggerInputParcel.class),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_INPUT));
WebTriggerInput input = new WebTriggerInput(inputParcel);
IDataAccessService binder = getDataAccessService(params);
UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
RequestToken requestToken = new RequestToken(binder, null, null, userData);
IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
isolatedWorker.onWebTrigger(
input,
new WrappedCallback<WebTriggerOutput, WebTriggerOutputParcel>(
resultCallback, requestToken, v -> new WebTriggerOutputParcel(v)));
} catch (Exception e) {
sLogger.e(e, TAG + ": Exception during Isolated Service web trigger operation.");
try {
resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
} catch (RemoteException re) {
sLogger.e(re, TAG + ": Isolated Service Callback failed.");
}
}
}
private void performOnTrainingExample(
@NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
try {
TrainingExamplesInputParcel inputParcel =
Objects.requireNonNull(
params.getParcelable(
Constants.EXTRA_INPUT, TrainingExamplesInputParcel.class),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_INPUT));
TrainingExamplesInput input = new TrainingExamplesInput(inputParcel);
IDataAccessService binder = getDataAccessService(params);
UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
RequestToken requestToken = new RequestToken(binder, null, null, userData);
IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
isolatedWorker.onTrainingExamples(
input,
new WrappedCallback<TrainingExamplesOutput, TrainingExamplesOutputParcel>(
resultCallback,
requestToken,
v ->
new TrainingExamplesOutputParcel.Builder()
.setTrainingExampleRecords(
new OdpParceledListSlice<
TrainingExampleRecord>(
v.getTrainingExampleRecords()))
.build()));
} catch (Exception e) {
sLogger.e(e,
TAG + ": Exception during Isolated Service training example operation.");
try {
resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
} catch (RemoteException re) {
sLogger.e(re, TAG + ": Isolated Service Callback failed.");
}
}
}
private void performOnWebViewEvent(
@NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
try {
EventInputParcel inputParcel =
Objects.requireNonNull(
params.getParcelable(Constants.EXTRA_INPUT, EventInputParcel.class),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_INPUT));
EventInput input = new EventInput(inputParcel);
IDataAccessService binder = getDataAccessService(params);
UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
RequestToken requestToken = new RequestToken(binder, null, null, userData);
IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
isolatedWorker.onEvent(
input,
new WrappedCallback<EventOutput, EventOutputParcel>(
resultCallback, requestToken, v -> new EventOutputParcel(v)));
} catch (Exception e) {
sLogger.e(e, TAG + ": Exception during Isolated Service web view event operation.");
try {
resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
} catch (RemoteException re) {
sLogger.e(re, TAG + ": Isolated Service Callback failed.");
}
}
}
private void performRender(
@NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
try {
RenderInputParcel inputParcel =
Objects.requireNonNull(
params.getParcelable(
Constants.EXTRA_INPUT, RenderInputParcel.class),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_INPUT));
RenderInput input = new RenderInput(inputParcel);
Objects.requireNonNull(input.getRenderingConfig());
IDataAccessService binder = getDataAccessService(params);
RequestToken requestToken = new RequestToken(binder, null, null, null);
IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
isolatedWorker.onRender(
input,
new WrappedCallback<RenderOutput, RenderOutputParcel>(
resultCallback, requestToken, v -> new RenderOutputParcel(v)));
} catch (Exception e) {
sLogger.e(e, TAG + ": Exception during Isolated Service render operation.");
try {
resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
} catch (RemoteException re) {
sLogger.e(re, TAG + ": Isolated Service Callback failed.");
}
}
}
private void performDownload(
@NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
try {
DownloadInputParcel inputParcel =
Objects.requireNonNull(
params.getParcelable(
Constants.EXTRA_INPUT, DownloadInputParcel.class),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_INPUT));
KeyValueStore downloadedContents =
new RemoteDataImpl(
IDataAccessService.Stub.asInterface(
Objects.requireNonNull(
inputParcel.getDataAccessServiceBinder(),
"Failed to get IDataAccessService binder from the input params!")));
DownloadCompletedInput input =
new DownloadCompletedInput.Builder()
.setDownloadedContents(downloadedContents)
.build();
IDataAccessService binder = getDataAccessService(params);
IFederatedComputeService fcBinder = getFederatedComputeService(params);
UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
RequestToken requestToken = new RequestToken(binder, fcBinder, null, userData);
IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
isolatedWorker.onDownloadCompleted(
input,
new WrappedCallback<DownloadCompletedOutput, DownloadCompletedOutputParcel>(
resultCallback,
requestToken,
v -> new DownloadCompletedOutputParcel(v)));
} catch (Exception e) {
sLogger.e(e, TAG + ": Exception during Isolated Service download operation.");
try {
resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
} catch (RemoteException re) {
sLogger.e(re, TAG + ": Isolated Service Callback failed.");
}
}
}
private static IIsolatedModelService getIsolatedModelService(@NonNull Bundle params) {
IIsolatedModelService modelServiceBinder =
IIsolatedModelService.Stub.asInterface(
Objects.requireNonNull(
params.getBinder(Constants.EXTRA_MODEL_SERVICE_BINDER),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_MODEL_SERVICE_BINDER)));
Objects.requireNonNull(
modelServiceBinder,
"Failed to get IIsolatedModelService binder from the input params!");
return modelServiceBinder;
}
private static IFederatedComputeService getFederatedComputeService(@NonNull Bundle params) {
IFederatedComputeService fcBinder =
IFederatedComputeService.Stub.asInterface(
Objects.requireNonNull(
params.getBinder(
Constants.EXTRA_FEDERATED_COMPUTE_SERVICE_BINDER),
() ->
String.format(
"Missing '%s' from input params!",
Constants
.EXTRA_FEDERATED_COMPUTE_SERVICE_BINDER)));
Objects.requireNonNull(
fcBinder,
"Failed to get IFederatedComputeService binder from the input params!");
return fcBinder;
}
private static IDataAccessService getDataAccessService(@NonNull Bundle params) {
IDataAccessService binder =
IDataAccessService.Stub.asInterface(
Objects.requireNonNull(
params.getBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER)));
Objects.requireNonNull(
binder, "Failed to get IDataAccessService binder from the input params!");
return binder;
}
private void performExecute(
@NonNull Bundle params, @NonNull IIsolatedServiceCallback resultCallback) {
try {
ExecuteInputParcel inputParcel =
Objects.requireNonNull(
params.getParcelable(
Constants.EXTRA_INPUT, ExecuteInputParcel.class),
() ->
String.format(
"Missing '%s' from input params!",
Constants.EXTRA_INPUT));
ExecuteInput input = new ExecuteInput(inputParcel);
Objects.requireNonNull(
input.getAppPackageName(),
"Failed to get AppPackageName from the input params!");
IDataAccessService binder = getDataAccessService(params);
IFederatedComputeService fcBinder = getFederatedComputeService(params);
IIsolatedModelService modelServiceBinder = getIsolatedModelService(params);
UserData userData = params.getParcelable(Constants.EXTRA_USER_DATA, UserData.class);
RequestToken requestToken =
new RequestToken(binder, fcBinder, modelServiceBinder, userData);
IsolatedWorker isolatedWorker = IsolatedService.this.onRequest(requestToken);
isolatedWorker.onExecute(
input,
new WrappedCallback<ExecuteOutput, ExecuteOutputParcel>(
resultCallback, requestToken, v -> new ExecuteOutputParcel(v)));
} catch (Exception e) {
sLogger.e(e, TAG + ": Exception during Isolated Service execute operation.");
try {
resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
} catch (RemoteException re) {
sLogger.e(re, TAG + ": Isolated Service Callback failed.");
}
}
}
}
private static class WrappedCallback<T, U extends Parcelable>
implements OutcomeReceiver<T, IsolatedServiceException> {
@NonNull private final IIsolatedServiceCallback mCallback;
@NonNull private final RequestToken mRequestToken;
@NonNull private final Function<T, U> mConverter;
WrappedCallback(
IIsolatedServiceCallback callback,
RequestToken requestToken,
Function<T, U> converter) {
mCallback = Objects.requireNonNull(callback);
mRequestToken = Objects.requireNonNull(requestToken);
mConverter = Objects.requireNonNull(converter);
}
@Override
public void onResult(T result) {
long elapsedTimeMillis =
SystemClock.elapsedRealtime() - mRequestToken.getStartTimeMillis();
if (result == null) {
try {
mCallback.onError(Constants.STATUS_SERVICE_FAILED, 0);
} catch (RemoteException e) {
sLogger.w(TAG + ": Callback failed.", e);
}
} else {
Bundle bundle = new Bundle();
U wrappedResult = mConverter.apply(result);
bundle.putParcelable(Constants.EXTRA_RESULT, wrappedResult);
bundle.putParcelable(Constants.EXTRA_CALLEE_METADATA,
new CalleeMetadata.Builder()
.setElapsedTimeMillis(elapsedTimeMillis)
.build());
try {
mCallback.onSuccess(bundle);
} catch (RemoteException e) {
sLogger.w(TAG + ": Callback failed.", e);
}
}
}
@Override
public void onError(IsolatedServiceException e) {
try {
// TODO(b/324478256): Log and report the error code from e.
mCallback.onError(Constants.STATUS_SERVICE_FAILED, e.getErrorCode());
} catch (RemoteException re) {
sLogger.w(TAG + ": Callback failed.", re);
}
}
}
}