blob: 5b83c6cc23d33e47628bd3408fdccb84d406504c [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.display;
import android.adservices.ondevicepersonalization.Constants;
import android.adservices.ondevicepersonalization.EventInput;
import android.adservices.ondevicepersonalization.EventLogRecord;
import android.adservices.ondevicepersonalization.EventOutput;
import android.adservices.ondevicepersonalization.RequestLogRecord;
import android.adservices.ondevicepersonalization.UserData;
import android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
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.data.events.Event;
import com.android.ondevicepersonalization.services.data.events.EventUrlHelper;
import com.android.ondevicepersonalization.services.data.events.EventUrlPayload;
import com.android.ondevicepersonalization.services.data.events.EventsDao;
import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
import com.android.ondevicepersonalization.services.policyengine.UserDataAccessor;
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.MonotonicClock;
import com.android.ondevicepersonalization.services.util.OnDevicePersonalizationFlatbufferUtils;
import com.android.ondevicepersonalization.services.util.StatsUtils;
import com.google.common.util.concurrent.FluentFuture;
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.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
class OdpWebViewClient extends WebViewClient {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
private static final String TAG = "OdpWebViewClient";
public static final String TASK_NAME = "ComputeEventMetrics";
@VisibleForTesting
static class Injector {
ListeningExecutorService getExecutor() {
return OnDevicePersonalizationExecutors.getBackgroundExecutor();
}
void openUrl(String landingPage, Context context) {
if (landingPage != null) {
sLogger.d(TAG + ": Sending intent to open landingPage: " + landingPage);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(landingPage));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
Clock getClock() {
return MonotonicClock.getInstance();
}
Flags getFlags() {
return FlagsFactory.getFlags();
}
ListeningScheduledExecutorService getScheduledExecutor() {
return OnDevicePersonalizationExecutors.getScheduledExecutor();
}
}
@NonNull private final Context mContext;
@NonNull private final String mServicePackageName;
long mQueryId;
@NonNull private final RequestLogRecord mLogRecord;
@NonNull private final Injector mInjector;
OdpWebViewClient(Context context, String servicePackageName, long queryId,
RequestLogRecord logRecord) {
this(context, servicePackageName, queryId, logRecord, new Injector());
}
@VisibleForTesting
OdpWebViewClient(Context context, String servicePackageName, long queryId,
RequestLogRecord logRecord, Injector injector) {
mContext = context;
mServicePackageName = servicePackageName;
mQueryId = queryId;
mLogRecord = logRecord;
mInjector = injector;
}
@Override public WebResourceResponse shouldInterceptRequest(
@NonNull WebView webView, @NonNull WebResourceRequest request) {
try {
if (webView == null || request == null || request.getUrl() == null) {
sLogger.e(TAG + ": Received null webView or Request or Url");
return null;
}
String url = request.getUrl().toString();
sLogger.d(TAG + ": shouldInterceptRequest: " + url);
if (EventUrlHelper.isOdpUrl(url)) {
EventUrlPayload payload = EventUrlHelper.getEventFromOdpEventUrl(url);
mInjector.getExecutor().execute(() -> handleEvent(payload));
byte[] responseData = payload.getResponseData();
if (responseData == null || responseData.length == 0) {
return new WebResourceResponse(
null, null, HttpURLConnection.HTTP_NO_CONTENT, "No Content",
Collections.emptyMap(), InputStream.nullInputStream());
} else {
return new WebResourceResponse(
payload.getMimeType(), null, HttpURLConnection.HTTP_OK, "OK",
Collections.emptyMap(),
new ByteArrayInputStream(responseData));
}
}
} catch (Exception e) {
sLogger.e(e, TAG + ": shouldInterceptRequest failed.");
}
return null;
}
@Override
public boolean shouldOverrideUrlLoading(
@NonNull WebView webView, @NonNull WebResourceRequest request) {
try {
if (webView == null || request == null) {
sLogger.e(TAG + ": Received null webView or Request");
return true;
}
//Decode odp://localhost/ URIs and call Events table API to write an event.
String url = request.getUrl().toString();
sLogger.d(TAG + ": shouldOverrideUrlLoading: " + url);
if (EventUrlHelper.isOdpUrl(url)) {
EventUrlPayload payload = EventUrlHelper.getEventFromOdpEventUrl(url);
mInjector.getExecutor().execute(() -> handleEvent(payload));
String landingPage = request.getUrl().getQueryParameter(
EventUrlHelper.URL_LANDING_PAGE_EVENT_KEY);
mInjector.openUrl(landingPage, webView.getContext());
} else {
// TODO(b/263180569): Handle any non-odp URLs
sLogger.d(TAG + ": Non-odp URL encountered: " + url);
}
} catch (Exception e) {
sLogger.e(e, TAG + ": shouldOverrideUrlLoading failed.");
}
// Cancel the current load
return true;
}
private ListenableFuture<EventOutput> executeEventHandler(
long startTimeMillis,
IsolatedServiceInfo isolatedServiceInfo,
EventUrlPayload payload) {
try {
sLogger.d(TAG + ": executeEventHandler() called");
Bundle serviceParams = new Bundle();
DataAccessServiceImpl binder = new DataAccessServiceImpl(
mServicePackageName, mContext, /* includeLocalData */ true,
/* includeEventData */ true);
serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, binder);
// TODO(b/259950177): Add Query row to input.
EventInput input = new EventInput.Builder()
.setParameters(payload.getEventParams())
.setRequestLogRecord(mLogRecord)
.build();
serviceParams.putParcelable(Constants.EXTRA_INPUT, input);
UserDataAccessor userDataAccessor = new UserDataAccessor();
UserData userData = userDataAccessor.getUserData();
serviceParams.putParcelable(Constants.EXTRA_USER_DATA, userData);
return FluentFuture.from(
ProcessUtils.runIsolatedService(
isolatedServiceInfo,
AppManifestConfigHelper.getServiceNameFromOdpSettings(
mContext, mServicePackageName),
Constants.OP_WEB_VIEW_EVENT,
serviceParams))
.transform(
result -> {
writeServiceRequestMetrics(
result, startTimeMillis, Constants.STATUS_SUCCESS);
return result.getParcelable(
Constants.EXTRA_RESULT, EventOutput.class);
},
mInjector.getExecutor())
.catchingAsync(
Exception.class,
e -> {
writeServiceRequestMetrics(
null, startTimeMillis, Constants.STATUS_INTERNAL_ERROR);
return Futures.immediateFailedFuture(e);
},
mInjector.getExecutor()
);
} catch (Exception e) {
sLogger.e(TAG + ": executeEventHandler() failed", e);
return Futures.immediateFailedFuture(e);
}
}
ListenableFuture<EventOutput> getEventOutput(EventUrlPayload payload) {
try {
sLogger.d(TAG + ": getEventOutput(): Starting isolated process.");
long startTimeMillis = mInjector.getClock().elapsedRealtime();
return FluentFuture.from(ProcessUtils.loadIsolatedService(
TASK_NAME, mServicePackageName, mContext))
.transformAsync(
result -> executeEventHandler(startTimeMillis, result, payload),
mInjector.getExecutor());
} catch (Exception e) {
sLogger.e(TAG + ": getEventOutput() failed", e);
return Futures.immediateFailedFuture(e);
}
}
private ListenableFuture<Void> writeEvent(EventOutput result) {
try {
sLogger.d(TAG + ": writeEvent() called. EventOutput: " + result.toString());
if (result == null || result.getEventLogRecord() == null) {
return Futures.immediateFuture(null);
}
EventLogRecord eventData = result.getEventLogRecord();
int rowCount = 0;
if (mLogRecord.getRows() != null) {
rowCount = mLogRecord.getRows().size();
}
if (eventData.getType() <= 0 || eventData.getRowIndex() < 0
|| eventData.getRowIndex() >= rowCount) {
sLogger.w(TAG + ": rowOffset out of range");
return Futures.immediateFuture(null);
}
byte[] data = OnDevicePersonalizationFlatbufferUtils.createEventData(
eventData.getData());
Event event = new Event.Builder()
.setType(eventData.getType())
.setQueryId(mQueryId)
.setServicePackageName(mServicePackageName)
.setTimeMillis(mInjector.getClock().currentTimeMillis())
.setRowIndex(eventData.getRowIndex())
.setEventData(data)
.build();
if (-1 == EventsDao.getInstance(mContext).insertEvent(event)) {
sLogger.e(TAG + ": Failed to insert event: " + event);
}
return Futures.immediateFuture(null);
} catch (Exception e) {
sLogger.e(TAG + ": writeEvent() failed", e);
return Futures.immediateFailedFuture(e);
}
}
private void handleEvent(EventUrlPayload eventUrlPayload) {
try {
sLogger.d(TAG + ": handleEvent() called");
var unused = FluentFuture.from(getEventOutput(eventUrlPayload))
.transformAsync(
result -> writeEvent(result),
mInjector.getExecutor())
.withTimeout(
mInjector.getFlags().getIsolatedServiceDeadlineSeconds(),
TimeUnit.SECONDS,
mInjector.getScheduledExecutor()
);
} catch (Exception e) {
sLogger.e(TAG + ": Failed to handle Event", e);
}
}
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_EVENT)
.setLatencyMillis(latencyMillis)
.setOverheadLatencyMillis(overheadLatencyMillis)
.setResponseCode(responseCode)
.build();
OdpStatsdLogger.getInstance().logApiCallStats(callStats);
}
}