blob: a4d788ccd7858c1ee853c6288847c415445af018 [file] [log] [blame]
/*
* Copyright (C) 2023 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 com.android.ondevicepersonalization.services.request;
import android.adservices.ondevicepersonalization.Constants;
import android.adservices.ondevicepersonalization.RenderInput;
import android.adservices.ondevicepersonalization.RenderOutput;
import android.adservices.ondevicepersonalization.RenderingConfig;
import android.adservices.ondevicepersonalization.RequestLogRecord;
import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.SurfaceControlViewHost.SurfacePackage;
import com.android.internal.annotations.VisibleForTesting;
import com.android.ondevicepersonalization.internal.util.LoggerFactory;
import com.android.ondevicepersonalization.services.Flags;
import com.android.ondevicepersonalization.services.FlagsFactory;
import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
import com.android.ondevicepersonalization.services.data.DataAccessServiceImpl;
import com.android.ondevicepersonalization.services.display.DisplayHelper;
import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
import com.android.ondevicepersonalization.services.process.IsolatedServiceInfo;
import com.android.ondevicepersonalization.services.process.ProcessUtils;
import com.android.ondevicepersonalization.services.statsd.ApiCallStats;
import com.android.ondevicepersonalization.services.statsd.OdpStatsdLogger;
import com.android.ondevicepersonalization.services.util.Clock;
import com.android.ondevicepersonalization.services.util.CryptUtils;
import com.android.ondevicepersonalization.services.util.MonotonicClock;
import com.android.ondevicepersonalization.services.util.StatsUtils;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Handles a surface package request from an app or SDK.
*/
public class RenderFlow {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
private static final String TAG = "RenderFlow";
private static final String TASK_NAME = "Render";
@VisibleForTesting
static class Injector {
ListeningExecutorService getExecutor() {
return OnDevicePersonalizationExecutors.getBackgroundExecutor();
}
SlotWrapper decryptToken(String slotResultToken) throws Exception {
return (SlotWrapper) CryptUtils.decrypt(slotResultToken);
}
Clock getClock() {
return MonotonicClock.getInstance();
}
Flags getFlags() {
return FlagsFactory.getFlags();
}
ListeningScheduledExecutorService getScheduledExecutor() {
return OnDevicePersonalizationExecutors.getScheduledExecutor();
}
}
@NonNull
private final String mSlotResultToken;
@NonNull
private final IBinder mHostToken;
@NonNull private final int mDisplayId;
@NonNull private final int mWidth;
@NonNull private final int mHeight;
@NonNull
private final IRequestSurfacePackageCallback mCallback;
@NonNull
private final Context mContext;
private final long mStartTimeMillis;
@NonNull
private final Injector mInjector;
@NonNull
private final DisplayHelper mDisplayHelper;
@NonNull
private String mServicePackageName;
@NonNull
private String mServiceClassName;
public RenderFlow(
@NonNull String slotResultToken,
@NonNull IBinder hostToken,
int displayId,
int width,
int height,
@NonNull IRequestSurfacePackageCallback callback,
@NonNull Context context,
long startTimeMillis) {
this(slotResultToken, hostToken, displayId, width, height,
callback, context, startTimeMillis,
new Injector(),
new DisplayHelper(context));
}
@VisibleForTesting
RenderFlow(
@NonNull String slotResultToken,
@NonNull IBinder hostToken,
int displayId,
int width,
int height,
@NonNull IRequestSurfacePackageCallback callback,
@NonNull Context context,
long startTimeMillis,
@NonNull Injector injector,
@NonNull DisplayHelper displayHelper) {
sLogger.d(TAG + ": RenderFlow created.");
mSlotResultToken = Objects.requireNonNull(slotResultToken);
mHostToken = Objects.requireNonNull(hostToken);
mDisplayId = displayId;
mWidth = width;
mHeight = height;
mCallback = Objects.requireNonNull(callback);
mStartTimeMillis = startTimeMillis;
mInjector = Objects.requireNonNull(injector);
mContext = Objects.requireNonNull(context);
mDisplayHelper = Objects.requireNonNull(displayHelper);
}
/** Runs the request processing flow. */
public void run() {
var unused = Futures.submit(() -> this.processRequest(), mInjector.getExecutor());
}
private void processRequest() {
try {
SlotWrapper slotWrapper = Objects.requireNonNull(
mInjector.decryptToken(mSlotResultToken));
mServicePackageName = Objects.requireNonNull(
slotWrapper.getServicePackageName());
mServiceClassName = Objects.requireNonNull(
AppManifestConfigHelper.getServiceNameFromOdpSettings(
mContext, mServicePackageName));
ListenableFuture<SurfacePackage> surfacePackageFuture =
FluentFuture.from(renderContentForSlot(slotWrapper))
.withTimeout(
mInjector.getFlags().getIsolatedServiceDeadlineSeconds(),
TimeUnit.SECONDS,
mInjector.getScheduledExecutor()
);
Futures.addCallback(
surfacePackageFuture,
new FutureCallback<SurfacePackage>() {
@Override
public void onSuccess(SurfacePackage surfacePackage) {
sendDisplayResult(surfacePackage);
}
@Override
public void onFailure(Throwable t) {
sLogger.w(TAG + ": Request failed.", t);
sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
}
},
mInjector.getExecutor());
} catch (Exception e) {
sLogger.e(TAG + ": Could not process request.", e);
sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
}
}
private ListenableFuture<SurfacePackage> renderContentForSlot(
@NonNull SlotWrapper slotWrapper
) {
try {
sLogger.d(TAG + ": renderContentForSlot() started.");
Objects.requireNonNull(slotWrapper);
RequestLogRecord logRecord = Objects.requireNonNull(slotWrapper.getLogRecord());
RenderingConfig renderingConfig =
Objects.requireNonNull(slotWrapper.getRenderingConfig());
long queryId = slotWrapper.getQueryId();
long serviceStartTimeMillis = mInjector.getClock().elapsedRealtime();
return FluentFuture.from(ProcessUtils.loadIsolatedService(
TASK_NAME, mServicePackageName, mContext))
.transformAsync(
loadResult -> executeRenderContentRequest(
serviceStartTimeMillis,
loadResult, slotWrapper.getSlotIndex(), renderingConfig),
mInjector.getExecutor())
.transform(result -> {
return result.getParcelable(
Constants.EXTRA_RESULT, RenderOutput.class);
}, mInjector.getExecutor())
.transform(
result -> mDisplayHelper.generateHtml(result, mServicePackageName),
mInjector.getExecutor())
.transformAsync(
result -> mDisplayHelper.displayHtml(
result,
logRecord,
queryId,
mServicePackageName,
mHostToken,
mDisplayId,
mWidth,
mHeight),
mInjector.getExecutor());
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
private ListenableFuture<Bundle> executeRenderContentRequest(
long serviceStartTimeMillis,
IsolatedServiceInfo isolatedServiceInfo, int slotIndex,
RenderingConfig renderingConfig) {
sLogger.d(TAG + "executeRenderContentRequest() started.");
Bundle serviceParams = new Bundle();
RenderInput input =
new RenderInput.Builder()
.setHeight(mHeight)
.setWidth(mWidth)
.setRenderingConfigIndex(slotIndex)
.setRenderingConfig(renderingConfig)
.build();
serviceParams.putParcelable(Constants.EXTRA_INPUT, input);
DataAccessServiceImpl binder = new DataAccessServiceImpl(
mServicePackageName, mContext, /* includeLocalData */ false,
/* includeEventData */ false);
serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, binder);
ListenableFuture<Bundle> result = ProcessUtils.runIsolatedService(
isolatedServiceInfo, mServiceClassName, Constants.OP_RENDER,
serviceParams);
return FluentFuture.from(result)
.transform(
val -> {
writeServiceRequestMetrics(
val, serviceStartTimeMillis, Constants.STATUS_SUCCESS);
return val;
},
mInjector.getExecutor()
)
.catchingAsync(
Exception.class,
e -> {
writeServiceRequestMetrics(
null, serviceStartTimeMillis, Constants.STATUS_INTERNAL_ERROR);
return Futures.immediateFailedFuture(e);
},
mInjector.getExecutor()
);
}
private void sendDisplayResult(SurfacePackage surfacePackage) {
if (surfacePackage != null) {
sendSuccessResult(surfacePackage);
} else {
sLogger.w(TAG + ": surfacePackages is null or empty");
sendErrorResult(Constants.STATUS_INTERNAL_ERROR);
}
}
private void sendSuccessResult(SurfacePackage surfacePackage) {
int responseCode = Constants.STATUS_SUCCESS;
try {
mCallback.onSuccess(surfacePackage);
} catch (RemoteException e) {
responseCode = Constants.STATUS_INTERNAL_ERROR;
sLogger.w(TAG + ": Callback error", e);
} finally {
writeAppRequestMetrics(responseCode);
}
}
private void sendErrorResult(int errorCode) {
try {
mCallback.onError(errorCode);
} catch (RemoteException e) {
sLogger.w(TAG + ": Callback error", e);
} finally {
writeAppRequestMetrics(errorCode);
}
}
private void writeAppRequestMetrics(int responseCode) {
int latencyMillis = (int) (mInjector.getClock().elapsedRealtime() - mStartTimeMillis);
ApiCallStats callStats = new ApiCallStats.Builder(ApiCallStats.API_REQUEST_SURFACE_PACKAGE)
.setLatencyMillis(latencyMillis)
.setResponseCode(responseCode)
.build();
OdpStatsdLogger.getInstance().logApiCallStats(callStats);
}
private void writeServiceRequestMetrics(Bundle result, long startTimeMillis, int responseCode) {
int latencyMillis = (int) (mInjector.getClock().elapsedRealtime() - startTimeMillis);
int overheadLatencyMillis =
(int) StatsUtils.getOverheadLatencyMillis(latencyMillis, result);
ApiCallStats callStats = new ApiCallStats.Builder(ApiCallStats.API_SERVICE_ON_RENDER)
.setLatencyMillis(latencyMillis)
.setOverheadLatencyMillis(overheadLatencyMillis)
.setResponseCode(responseCode)
.build();
OdpStatsdLogger.getInstance().logApiCallStats(callStats);
}
}