blob: f38cff47aa494c553355e06ab35fee8931859a0e [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.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.ondevicepersonalization.Bid;
import android.ondevicepersonalization.Constants;
import android.ondevicepersonalization.EventInput;
import android.ondevicepersonalization.EventOutput;
import android.ondevicepersonalization.Metrics;
import android.ondevicepersonalization.SlotResult;
import android.os.Bundle;
import android.util.Log;
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.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.process.IsolatedServiceInfo;
import com.android.ondevicepersonalization.services.process.ProcessUtils;
import com.android.ondevicepersonalization.services.util.OnDevicePersonalizationFlatbufferUtils;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.HashMap;
import java.util.concurrent.Executor;
class OdpWebViewClient extends WebViewClient {
private static final String TAG = "OdpWebViewClient";
public static final String TASK_NAME = "ComputeEventMetrics";
@VisibleForTesting
static class Injector {
Executor getExecutor() {
return OnDevicePersonalizationExecutors.getBackgroundExecutor();
}
void openUrl(String landingPage, Context context) {
if (landingPage != null) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(landingPage));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
}
@NonNull private final Context mContext;
@NonNull private final String mServicePackageName;
@NonNull private final HashMap<String, Bid> mBidsMap = new HashMap<>();
@NonNull private final Injector mInjector;
OdpWebViewClient(Context context, String servicePackageName, SlotResult slotResult) {
this(context, servicePackageName, slotResult, new Injector());
}
@VisibleForTesting
OdpWebViewClient(Context context, String servicePackageName, SlotResult slotResult,
Injector injector) {
mContext = context;
mServicePackageName = servicePackageName;
for (Bid bid: slotResult.getLoggedBids()) {
mBidsMap.put(bid.getKey(), bid);
}
mInjector = injector;
}
@Override public WebResourceResponse shouldInterceptRequest(
@NonNull WebView webView, @NonNull WebResourceRequest request) {
if (webView == null || request == null || request.getUrl() == null) {
Log.e(TAG, "Received null webView or Request or Url");
return null;
}
String url = request.getUrl().toString();
if (EventUrlHelper.isOdpUrl(url)) {
mInjector.getExecutor().execute(() -> handleEvent(url));
// TODO(b/242753206): Return an empty response.
}
return null;
}
@Override
public boolean shouldOverrideUrlLoading(
@NonNull WebView webView, @NonNull WebResourceRequest request) {
if (webView == null || request == null) {
Log.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();
if (EventUrlHelper.isOdpUrl(url)) {
mInjector.getExecutor().execute(() -> handleEvent(url));
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
Log.d(TAG, "Non-odp URL encountered: " + url);
}
// Cancel the current load
return true;
}
private ListenableFuture<EventOutput> executeEventHandler(
IsolatedServiceInfo isolatedServiceInfo, EventUrlPayload payload) {
try {
Log.d(TAG, "executeEventHandler() called");
Bundle serviceParams = new Bundle();
DataAccessServiceImpl binder = new DataAccessServiceImpl(
mServicePackageName, mContext, true, null);
serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, binder);
Bid bid = mBidsMap.get(payload.getEvent().getBidId());
// TODO(b/259950177): Add Query row to input.
EventInput input = new EventInput.Builder()
.setEventType(payload.getEvent().getType())
.setBid(bid)
.build();
serviceParams.putParcelable(Constants.EXTRA_INPUT, input);
return FluentFuture.from(
ProcessUtils.runIsolatedService(
isolatedServiceInfo,
AppManifestConfigHelper.getServiceNameFromOdpSettings(
mContext, mServicePackageName),
Constants.OP_EVENT,
serviceParams))
.transform(
result -> result.getParcelable(
Constants.EXTRA_RESULT, EventOutput.class),
mInjector.getExecutor());
} catch (Exception e) {
Log.e(TAG, "executeEventHandler() failed", e);
return Futures.immediateFailedFuture(e);
}
}
ListenableFuture<EventOutput> getEventMetrics(EventUrlPayload payload) {
try {
Log.d(TAG, "getEventMetrics(): Starting isolated process.");
return FluentFuture.from(ProcessUtils.loadIsolatedService(
TASK_NAME, mServicePackageName, mContext))
.transformAsync(
result -> executeEventHandler(result, payload),
mInjector.getExecutor());
} catch (Exception e) {
Log.e(TAG, "getEventMetrics() failed", e);
return Futures.immediateFailedFuture(e);
}
}
private ListenableFuture<Void> writeEvent(Event event, EventOutput result) {
try {
Log.d(TAG, "writeEvent() called. event: " + event.toString() + " metrics: "
+ result.toString());
Metrics metrics = null;
if (result != null) {
metrics = result.getMetrics();
}
if (metrics == null) {
// Metrics required because eventData column is non-null.
metrics = new Metrics.Builder().build();
}
byte[] eventData = OnDevicePersonalizationFlatbufferUtils.createEventData(metrics);
event = new Event.Builder()
.setType(event.getType())
.setQueryId(event.getQueryId())
.setServicePackageName(event.getServicePackageName())
.setTimeMillis(event.getTimeMillis())
.setSlotId(event.getSlotId())
.setSlotPosition(event.getSlotPosition())
.setSlotIndex(event.getSlotIndex())
.setBidId(event.getBidId())
.setEventData(eventData)
.build();
if (-1 == EventsDao.getInstance(mContext).insertEvent(event)) {
Log.e(TAG, "Failed to insert event: " + event);
}
return Futures.immediateFuture(null);
} catch (Exception e) {
Log.e(TAG, "writeEvent() failed", e);
return Futures.immediateFailedFuture(e);
}
}
private void handleEvent(String url) {
try {
Log.d(TAG, "handleEvent() called");
EventUrlPayload eventUrlPayload = EventUrlHelper.getEventFromOdpEventUrl(url);
Event event = eventUrlPayload.getEvent();
var unused = FluentFuture.from(getEventMetrics(eventUrlPayload))
.transformAsync(
result -> writeEvent(event, result),
mInjector.getExecutor());
} catch (Exception e) {
Log.e(TAG, "Failed to handle Event", e);
}
}
}