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