435 lines
21 KiB
Java
435 lines
21 KiB
Java
/*
|
|
* 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.IExecuteCallback;
|
|
import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService;
|
|
import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
|
|
import android.annotation.CallbackExecutor;
|
|
import android.annotation.FlaggedApi;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.pm.PackageManager;
|
|
import android.os.Binder;
|
|
import android.os.Bundle;
|
|
import android.os.IBinder;
|
|
import android.os.OutcomeReceiver;
|
|
import android.os.PersistableBundle;
|
|
import android.os.SystemClock;
|
|
import android.view.SurfaceControlViewHost;
|
|
|
|
import com.android.adservices.ondevicepersonalization.flags.Flags;
|
|
import com.android.federatedcompute.internal.util.AbstractServiceBinder;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.modules.utils.build.SdkLevel;
|
|
import com.android.ondevicepersonalization.internal.util.ByteArrayParceledSlice;
|
|
import com.android.ondevicepersonalization.internal.util.LoggerFactory;
|
|
import com.android.ondevicepersonalization.internal.util.PersistableBundleUtils;
|
|
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.Executor;
|
|
|
|
// TODO(b/289102463): Add a link to the public ODP developer documentation.
|
|
/**
|
|
* OnDevicePersonalizationManager provides APIs for apps to load an
|
|
* {@link IsolatedService} in an isolated process and interact with it.
|
|
*
|
|
* An app can request an {@link IsolatedService} to generate content for display
|
|
* within an {@link android.view.SurfaceView} within the app's view hierarchy, and also 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. The displayed
|
|
* content and the persistent output are both not directly accessible by the calling app.
|
|
*/
|
|
@FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
|
|
public class OnDevicePersonalizationManager {
|
|
/** @hide */
|
|
public static final String ON_DEVICE_PERSONALIZATION_SERVICE =
|
|
"on_device_personalization_service";
|
|
private static final String INTENT_FILTER_ACTION = "android.OnDevicePersonalizationService";
|
|
private static final String ODP_MANAGING_SERVICE_PACKAGE_SUFFIX =
|
|
"com.android.ondevicepersonalization.services";
|
|
|
|
private static final String ALT_ODP_MANAGING_SERVICE_PACKAGE_SUFFIX =
|
|
"com.google.android.ondevicepersonalization.services";
|
|
private static final String TAG = OnDevicePersonalizationManager.class.getSimpleName();
|
|
private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
|
|
private final AbstractServiceBinder<IOnDevicePersonalizationManagingService> mServiceBinder;
|
|
private final Context mContext;
|
|
|
|
/**
|
|
* The result of a call to {@link OnDevicePersonalizationManager#execute(ComponentName,
|
|
* PersistableBundle, Executor, OutcomeReceiver)}
|
|
*/
|
|
public static class ExecuteResult {
|
|
@Nullable private final SurfacePackageToken mSurfacePackageToken;
|
|
@Nullable private final byte[] mOutputData;
|
|
|
|
/** @hide */
|
|
ExecuteResult(
|
|
@Nullable SurfacePackageToken surfacePackageToken,
|
|
@Nullable byte[] outputData) {
|
|
mSurfacePackageToken = surfacePackageToken;
|
|
mOutputData = outputData;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@link SurfacePackageToken}, which is an opaque reference to content that
|
|
* can be displayed in a {@link android.view.SurfaceView}. This may be null if the
|
|
* {@link IsolatedService} has not generated any content to be displayed within the
|
|
* calling app.
|
|
*/
|
|
@Nullable public SurfacePackageToken getSurfacePackageToken() {
|
|
return mSurfacePackageToken;
|
|
}
|
|
|
|
/**
|
|
* Returns the output data that was returned by the {@link IsolatedService}. This will be
|
|
* non-null if the {@link IsolatedService} returns any results to the caller, and the
|
|
* egress of data from the {@link IsolatedService} to the specific calling app is allowed
|
|
* by policy as well as an allowlist.
|
|
*/
|
|
@Nullable public byte[] getOutputData() {
|
|
return mOutputData;
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
public OnDevicePersonalizationManager(Context context) {
|
|
this(
|
|
context,
|
|
AbstractServiceBinder.getServiceBinderByIntent(
|
|
context,
|
|
INTENT_FILTER_ACTION,
|
|
List.of(
|
|
ODP_MANAGING_SERVICE_PACKAGE_SUFFIX,
|
|
ALT_ODP_MANAGING_SERVICE_PACKAGE_SUFFIX),
|
|
SdkLevel.isAtLeastU() ? Context.BIND_ALLOW_ACTIVITY_STARTS : 0,
|
|
IOnDevicePersonalizationManagingService.Stub::asInterface));
|
|
}
|
|
|
|
/** @hide */
|
|
@VisibleForTesting
|
|
public OnDevicePersonalizationManager(
|
|
Context context,
|
|
AbstractServiceBinder<IOnDevicePersonalizationManagingService> serviceBinder) {
|
|
mContext = context;
|
|
mServiceBinder = serviceBinder;
|
|
}
|
|
|
|
/**
|
|
* Executes an {@link IsolatedService} in the OnDevicePersonalization sandbox. The
|
|
* platform binds to the specified {@link IsolatedService} in an isolated process
|
|
* and calls {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}
|
|
* with the caller-provided parameters. When the {@link IsolatedService} finishes execution,
|
|
* the platform returns tokens that refer to the results from the service to the caller.
|
|
* These tokens can be subsequently used to display results in a
|
|
* {@link android.view.SurfaceView} within the calling app.
|
|
*
|
|
* @param service The {@link ComponentName} of the {@link IsolatedService}.
|
|
* @param params a {@link PersistableBundle} that is passed from the calling app to the
|
|
* {@link IsolatedService}. The expected contents of this parameter are defined
|
|
* by the{@link IsolatedService}. The platform does not interpret this parameter.
|
|
* @param executor the {@link Executor} on which to invoke the callback.
|
|
* @param receiver This returns a {@link ExecuteResult} object on success or an
|
|
* {@link Exception} on failure. If the
|
|
* {@link IsolatedService} returned a {@link RenderingConfig} to be displayed,
|
|
* {@link ExecuteResult#getSurfacePackageToken()} will return a non-null
|
|
* {@link SurfacePackageToken}.
|
|
* The {@link SurfacePackageToken} object can be used in a subsequent
|
|
* {@link #requestSurfacePackage(SurfacePackageToken, IBinder, int, int, int, Executor,
|
|
* OutcomeReceiver)} call to display the result in a view. The returned
|
|
* {@link SurfacePackageToken} may be null to indicate that no output is expected to be
|
|
* displayed for this request. If the {@link IsolatedService} has returned any output data
|
|
* and the calling app is allowlisted to receive data from this service, the
|
|
* {@link ExecuteResult#getOutputData()} will return a non-null byte array.
|
|
*
|
|
* In case of an error, the receiver returns one of the following exceptions:
|
|
* Returns a {@link android.content.pm.PackageManager.NameNotFoundException} if the handler
|
|
* package is not installed or does not have a valid ODP manifest.
|
|
* Returns {@link ClassNotFoundException} if the handler class is not found.
|
|
* Returns an {@link OnDevicePersonalizationException} if execution of the handler fails.
|
|
*/
|
|
public void execute(
|
|
@NonNull ComponentName service,
|
|
@NonNull PersistableBundle params,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull OutcomeReceiver<ExecuteResult, Exception> receiver
|
|
) {
|
|
Objects.requireNonNull(service);
|
|
Objects.requireNonNull(params);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(receiver);
|
|
Objects.requireNonNull(service.getPackageName());
|
|
Objects.requireNonNull(service.getClassName());
|
|
if (service.getPackageName().isEmpty()) {
|
|
throw new IllegalArgumentException("missing service package name");
|
|
}
|
|
if (service.getClassName().isEmpty()) {
|
|
throw new IllegalArgumentException("missing service class name");
|
|
}
|
|
long startTimeMillis = SystemClock.elapsedRealtime();
|
|
|
|
try {
|
|
final IOnDevicePersonalizationManagingService odpService =
|
|
mServiceBinder.getService(executor);
|
|
|
|
try {
|
|
IExecuteCallback callbackWrapper = new IExecuteCallback.Stub() {
|
|
@Override
|
|
public void onSuccess(
|
|
Bundle callbackResult) {
|
|
final long token = Binder.clearCallingIdentity();
|
|
try {
|
|
executor.execute(() -> {
|
|
try {
|
|
SurfacePackageToken surfacePackageToken = null;
|
|
if (callbackResult != null) {
|
|
String tokenString = callbackResult.getString(
|
|
Constants.EXTRA_SURFACE_PACKAGE_TOKEN_STRING);
|
|
if (tokenString != null && !tokenString.isBlank()) {
|
|
surfacePackageToken = new SurfacePackageToken(
|
|
tokenString);
|
|
}
|
|
}
|
|
byte[] data = callbackResult.getByteArray(
|
|
Constants.EXTRA_OUTPUT_DATA);
|
|
receiver.onResult(
|
|
new ExecuteResult(surfacePackageToken, data));
|
|
} catch (Exception e) {
|
|
receiver.onError(e);
|
|
}
|
|
});
|
|
} finally {
|
|
Binder.restoreCallingIdentity(token);
|
|
logApiCallStats(
|
|
odpService,
|
|
Constants.API_NAME_EXECUTE,
|
|
SystemClock.elapsedRealtime() - startTimeMillis,
|
|
Constants.STATUS_SUCCESS);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onError(
|
|
int errorCode, int isolatedServiceErrorCode, String message) {
|
|
final long token = Binder.clearCallingIdentity();
|
|
try {
|
|
executor.execute(() -> receiver.onError(
|
|
createException(
|
|
errorCode, isolatedServiceErrorCode, message)));
|
|
} finally {
|
|
Binder.restoreCallingIdentity(token);
|
|
logApiCallStats(
|
|
odpService,
|
|
Constants.API_NAME_EXECUTE,
|
|
SystemClock.elapsedRealtime() - startTimeMillis,
|
|
errorCode);
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
Bundle wrappedParams = new Bundle();
|
|
wrappedParams.putParcelable(
|
|
Constants.EXTRA_APP_PARAMS_SERIALIZED,
|
|
new ByteArrayParceledSlice(PersistableBundleUtils.toByteArray(params)));
|
|
odpService.execute(
|
|
mContext.getPackageName(),
|
|
service,
|
|
wrappedParams,
|
|
new CallerMetadata.Builder().setStartTimeMillis(startTimeMillis).build(),
|
|
callbackWrapper);
|
|
|
|
} catch (Exception e) {
|
|
logApiCallStats(
|
|
odpService,
|
|
Constants.API_NAME_EXECUTE,
|
|
SystemClock.elapsedRealtime() - startTimeMillis,
|
|
Constants.STATUS_INTERNAL_ERROR);
|
|
receiver.onError(e);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
receiver.onError(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Requests a {@link android.view.SurfaceControlViewHost.SurfacePackage} to be inserted into a
|
|
* {@link android.view.SurfaceView} inside the calling app. The surface package will contain an
|
|
* {@link android.view.View} with the content from a result of a prior call to
|
|
* {@code #execute(ComponentName, PersistableBundle, Executor, OutcomeReceiver)} running in
|
|
* the OnDevicePersonalization sandbox.
|
|
*
|
|
* @param surfacePackageToken a reference to a {@link SurfacePackageToken} returned by a prior
|
|
* call to {@code #execute(ComponentName, PersistableBundle, Executor, OutcomeReceiver)}.
|
|
* @param surfaceViewHostToken the hostToken of the {@link android.view.SurfaceView}, which is
|
|
* returned by {@link android.view.SurfaceView#getHostToken()} after the
|
|
* {@link android.view.SurfaceView} has been added to the view hierarchy.
|
|
* @param displayId the integer ID of the logical display on which to display the
|
|
* {@link android.view.SurfaceControlViewHost.SurfacePackage}, returned by
|
|
* {@code Context.getDisplay().getDisplayId()}.
|
|
* @param width the width of the {@link android.view.SurfaceControlViewHost.SurfacePackage}
|
|
* in pixels.
|
|
* @param height the height of the {@link android.view.SurfaceControlViewHost.SurfacePackage}
|
|
* in pixels.
|
|
* @param executor the {@link Executor} on which to invoke the callback
|
|
* @param receiver This either returns a
|
|
* {@link android.view.SurfaceControlViewHost.SurfacePackage} on success, or
|
|
* {@link Exception} on failure. The exception type is
|
|
* {@link OnDevicePersonalizationException} if execution of the handler fails.
|
|
*/
|
|
public void requestSurfacePackage(
|
|
@NonNull SurfacePackageToken surfacePackageToken,
|
|
@NonNull IBinder surfaceViewHostToken,
|
|
int displayId,
|
|
int width,
|
|
int height,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull OutcomeReceiver<SurfaceControlViewHost.SurfacePackage, Exception> receiver
|
|
) {
|
|
Objects.requireNonNull(surfacePackageToken);
|
|
Objects.requireNonNull(surfaceViewHostToken);
|
|
Objects.requireNonNull(executor);
|
|
Objects.requireNonNull(receiver);
|
|
if (width <= 0) {
|
|
throw new IllegalArgumentException("width must be > 0");
|
|
}
|
|
|
|
if (height <= 0) {
|
|
throw new IllegalArgumentException("height must be > 0");
|
|
}
|
|
|
|
if (displayId < 0) {
|
|
throw new IllegalArgumentException("displayId must be >= 0");
|
|
}
|
|
long startTimeMillis = SystemClock.elapsedRealtime();
|
|
|
|
try {
|
|
final IOnDevicePersonalizationManagingService service =
|
|
Objects.requireNonNull(mServiceBinder.getService(executor));
|
|
|
|
try {
|
|
IRequestSurfacePackageCallback callbackWrapper =
|
|
new IRequestSurfacePackageCallback.Stub() {
|
|
@Override
|
|
public void onSuccess(
|
|
SurfaceControlViewHost.SurfacePackage surfacePackage) {
|
|
final long token = Binder.clearCallingIdentity();
|
|
try {
|
|
executor.execute(() -> {
|
|
receiver.onResult(surfacePackage);
|
|
});
|
|
} finally {
|
|
Binder.restoreCallingIdentity(token);
|
|
logApiCallStats(
|
|
service,
|
|
Constants.API_NAME_REQUEST_SURFACE_PACKAGE,
|
|
SystemClock.elapsedRealtime() - startTimeMillis,
|
|
Constants.STATUS_SUCCESS);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onError(
|
|
int errorCode, int isolatedServiceErrorCode, String message) {
|
|
final long token = Binder.clearCallingIdentity();
|
|
try {
|
|
executor.execute(
|
|
() -> receiver.onError(createException(
|
|
errorCode, isolatedServiceErrorCode,
|
|
message)));
|
|
} finally {
|
|
Binder.restoreCallingIdentity(token);
|
|
logApiCallStats(
|
|
service,
|
|
Constants.API_NAME_REQUEST_SURFACE_PACKAGE,
|
|
SystemClock.elapsedRealtime() - startTimeMillis,
|
|
errorCode);
|
|
}
|
|
}
|
|
};
|
|
|
|
service.requestSurfacePackage(
|
|
surfacePackageToken.getTokenString(),
|
|
surfaceViewHostToken,
|
|
displayId,
|
|
width,
|
|
height,
|
|
new CallerMetadata.Builder().setStartTimeMillis(startTimeMillis).build(),
|
|
callbackWrapper);
|
|
|
|
} catch (Exception e) {
|
|
logApiCallStats(
|
|
service,
|
|
Constants.API_NAME_REQUEST_SURFACE_PACKAGE,
|
|
SystemClock.elapsedRealtime() - startTimeMillis,
|
|
Constants.STATUS_INTERNAL_ERROR);
|
|
receiver.onError(e);
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
receiver.onError(e);
|
|
}
|
|
}
|
|
|
|
private Exception createException(
|
|
int errorCode, int isolatedServiceErrorCode, String message) {
|
|
if (message == null || message.isBlank()) {
|
|
message = "Error: " + errorCode;
|
|
}
|
|
if (errorCode == Constants.STATUS_NAME_NOT_FOUND) {
|
|
return new PackageManager.NameNotFoundException();
|
|
} else if (errorCode == Constants.STATUS_CLASS_NOT_FOUND) {
|
|
return new ClassNotFoundException();
|
|
} else if (errorCode == Constants.STATUS_SERVICE_FAILED) {
|
|
if (isolatedServiceErrorCode > 0 && isolatedServiceErrorCode < 128) {
|
|
return new OnDevicePersonalizationException(
|
|
OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
|
|
new IsolatedServiceException(isolatedServiceErrorCode));
|
|
} else {
|
|
return new OnDevicePersonalizationException(
|
|
OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
|
|
message);
|
|
}
|
|
} else if (errorCode == Constants.STATUS_PERSONALIZATION_DISABLED) {
|
|
return new OnDevicePersonalizationException(
|
|
OnDevicePersonalizationException.ERROR_PERSONALIZATION_DISABLED,
|
|
message);
|
|
} else {
|
|
return new IllegalStateException(message);
|
|
}
|
|
}
|
|
|
|
private void logApiCallStats(
|
|
IOnDevicePersonalizationManagingService service,
|
|
int apiName,
|
|
long latencyMillis,
|
|
int responseCode) {
|
|
try {
|
|
if (service != null) {
|
|
service.logApiCallStats(apiName, latencyMillis, responseCode);
|
|
}
|
|
} catch (Exception e) {
|
|
sLogger.e(e, TAG + ": Error logging API call stats");
|
|
}
|
|
}
|
|
}
|