• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ondevicepersonalization.services.display;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.net.Uri;
23 import android.ondevicepersonalization.Bid;
24 import android.ondevicepersonalization.Constants;
25 import android.ondevicepersonalization.EventInput;
26 import android.ondevicepersonalization.EventOutput;
27 import android.ondevicepersonalization.Metrics;
28 import android.ondevicepersonalization.SlotResult;
29 import android.os.Bundle;
30 import android.util.Log;
31 import android.webkit.WebResourceRequest;
32 import android.webkit.WebResourceResponse;
33 import android.webkit.WebView;
34 import android.webkit.WebViewClient;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
38 import com.android.ondevicepersonalization.services.data.DataAccessServiceImpl;
39 import com.android.ondevicepersonalization.services.data.events.Event;
40 import com.android.ondevicepersonalization.services.data.events.EventUrlHelper;
41 import com.android.ondevicepersonalization.services.data.events.EventUrlPayload;
42 import com.android.ondevicepersonalization.services.data.events.EventsDao;
43 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
44 import com.android.ondevicepersonalization.services.process.IsolatedServiceInfo;
45 import com.android.ondevicepersonalization.services.process.ProcessUtils;
46 import com.android.ondevicepersonalization.services.util.OnDevicePersonalizationFlatbufferUtils;
47 
48 import com.google.common.util.concurrent.FluentFuture;
49 import com.google.common.util.concurrent.Futures;
50 import com.google.common.util.concurrent.ListenableFuture;
51 
52 import java.util.HashMap;
53 import java.util.concurrent.Executor;
54 
55 class OdpWebViewClient extends WebViewClient {
56     private static final String TAG = "OdpWebViewClient";
57     public static final String TASK_NAME = "ComputeEventMetrics";
58 
59     @VisibleForTesting
60     static class Injector {
getExecutor()61         Executor getExecutor() {
62             return OnDevicePersonalizationExecutors.getBackgroundExecutor();
63         }
64 
openUrl(String landingPage, Context context)65         void openUrl(String landingPage, Context context) {
66             if (landingPage != null) {
67                 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(landingPage));
68                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
69                 context.startActivity(intent);
70             }
71         }
72     }
73 
74     @NonNull private final Context mContext;
75     @NonNull private final String mServicePackageName;
76     @NonNull private final HashMap<String, Bid> mBidsMap = new HashMap<>();
77     @NonNull private final Injector mInjector;
78 
OdpWebViewClient(Context context, String servicePackageName, SlotResult slotResult)79     OdpWebViewClient(Context context, String servicePackageName, SlotResult slotResult) {
80         this(context, servicePackageName, slotResult, new Injector());
81     }
82 
83     @VisibleForTesting
OdpWebViewClient(Context context, String servicePackageName, SlotResult slotResult, Injector injector)84     OdpWebViewClient(Context context, String servicePackageName, SlotResult slotResult,
85             Injector injector) {
86         mContext = context;
87         mServicePackageName = servicePackageName;
88         for (Bid bid: slotResult.getLoggedBids()) {
89             mBidsMap.put(bid.getKey(), bid);
90         }
91         mInjector = injector;
92     }
93 
shouldInterceptRequest( @onNull WebView webView, @NonNull WebResourceRequest request)94     @Override public WebResourceResponse shouldInterceptRequest(
95         @NonNull WebView webView, @NonNull WebResourceRequest request) {
96         if (webView == null || request == null || request.getUrl() == null) {
97             Log.e(TAG, "Received null webView or Request or Url");
98             return null;
99         }
100         String url = request.getUrl().toString();
101         if (EventUrlHelper.isOdpUrl(url)) {
102             mInjector.getExecutor().execute(() -> handleEvent(url));
103             // TODO(b/242753206): Return an empty response.
104         }
105         return null;
106     }
107 
108     @Override
shouldOverrideUrlLoading( @onNull WebView webView, @NonNull WebResourceRequest request)109     public boolean shouldOverrideUrlLoading(
110             @NonNull WebView webView, @NonNull WebResourceRequest request) {
111         if (webView == null || request == null) {
112             Log.e(TAG, "Received null webView or Request");
113             return true;
114         }
115         //Decode odp://localhost/ URIs and call Events table API to write an event.
116         String url = request.getUrl().toString();
117         if (EventUrlHelper.isOdpUrl(url)) {
118             mInjector.getExecutor().execute(() -> handleEvent(url));
119             String landingPage = request.getUrl().getQueryParameter(
120                     EventUrlHelper.URL_LANDING_PAGE_EVENT_KEY);
121             mInjector.openUrl(landingPage, webView.getContext());
122         } else {
123             // TODO(b/263180569): Handle any non-odp URLs
124             Log.d(TAG, "Non-odp URL encountered: " + url);
125         }
126         // Cancel the current load
127         return true;
128     }
129 
executeEventHandler( IsolatedServiceInfo isolatedServiceInfo, EventUrlPayload payload)130     private ListenableFuture<EventOutput> executeEventHandler(
131             IsolatedServiceInfo isolatedServiceInfo, EventUrlPayload payload) {
132         try {
133             Log.d(TAG, "executeEventHandler() called");
134             Bundle serviceParams = new Bundle();
135             DataAccessServiceImpl binder = new DataAccessServiceImpl(
136                     mServicePackageName, mContext, true, null);
137             serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, binder);
138             Bid bid = mBidsMap.get(payload.getEvent().getBidId());
139             // TODO(b/259950177): Add Query row to input.
140             EventInput input = new EventInput.Builder()
141                     .setEventType(payload.getEvent().getType())
142                     .setBid(bid)
143                     .build();
144             serviceParams.putParcelable(Constants.EXTRA_INPUT, input);
145             return FluentFuture.from(
146                     ProcessUtils.runIsolatedService(
147                         isolatedServiceInfo,
148                         AppManifestConfigHelper.getServiceNameFromOdpSettings(
149                                 mContext, mServicePackageName),
150                         Constants.OP_COMPUTE_EVENT_METRICS,
151                         serviceParams))
152                     .transform(
153                             result -> result.getParcelable(
154                                 Constants.EXTRA_RESULT, EventOutput.class),
155                             mInjector.getExecutor());
156         } catch (Exception e) {
157             Log.e(TAG, "executeEventHandler() failed", e);
158             return Futures.immediateFailedFuture(e);
159         }
160 
161     }
162 
getEventMetrics(EventUrlPayload payload)163     ListenableFuture<EventOutput> getEventMetrics(EventUrlPayload payload) {
164         try {
165             Log.d(TAG, "getEventMetrics(): Starting isolated process.");
166             return FluentFuture.from(ProcessUtils.loadIsolatedService(
167                     TASK_NAME, mServicePackageName, mContext))
168                 .transformAsync(
169                         result -> executeEventHandler(result, payload),
170                         mInjector.getExecutor());
171 
172         } catch (Exception e) {
173             Log.e(TAG, "getEventMetrics() failed", e);
174             return Futures.immediateFailedFuture(e);
175         }
176     }
177 
writeEvent(Event event, EventOutput result)178     private ListenableFuture<Void> writeEvent(Event event, EventOutput result) {
179         try {
180             Log.d(TAG, "writeEvent() called. event: " + event.toString() + " metrics: "
181                      + result.toString());
182             Metrics metrics = null;
183             if (result != null) {
184                 metrics = result.getMetrics();
185             }
186             if (metrics == null) {
187                 // Metrics required because eventData column is non-null.
188                 metrics = new Metrics.Builder().build();
189             }
190             byte[] eventData = OnDevicePersonalizationFlatbufferUtils.createEventData(metrics);
191             event = new Event.Builder()
192                     .setType(event.getType())
193                     .setQueryId(event.getQueryId())
194                     .setServicePackageName(event.getServicePackageName())
195                     .setTimeMillis(event.getTimeMillis())
196                     .setSlotId(event.getSlotId())
197                     .setSlotPosition(event.getSlotPosition())
198                     .setSlotIndex(event.getSlotIndex())
199                     .setBidId(event.getBidId())
200                     .setEventData(eventData)
201                     .build();
202             if (-1 == EventsDao.getInstance(mContext).insertEvent(event)) {
203                 Log.e(TAG, "Failed to insert event: " + event);
204             }
205             return Futures.immediateFuture(null);
206         } catch (Exception e) {
207             Log.e(TAG, "writeEvent() failed", e);
208             return Futures.immediateFailedFuture(e);
209         }
210     }
211 
handleEvent(String url)212     private void handleEvent(String url) {
213         try {
214             Log.d(TAG, "handleEvent() called");
215             EventUrlPayload eventUrlPayload = EventUrlHelper.getEventFromOdpEventUrl(url);
216             Event event = eventUrlPayload.getEvent();
217 
218             var unused = FluentFuture.from(getEventMetrics(eventUrlPayload))
219                     .transformAsync(
220                         result -> writeEvent(event, result),
221                         mInjector.getExecutor());
222 
223         } catch (Exception e) {
224             Log.e(TAG, "Failed to handle Event", e);
225         }
226     }
227 }
228