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.adservices.ondevicepersonalization.EventOutputParcel; 20 import android.adservices.ondevicepersonalization.RequestLogRecord; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.net.Uri; 27 import android.webkit.WebResourceRequest; 28 import android.webkit.WebResourceResponse; 29 import android.webkit.WebView; 30 import android.webkit.WebViewClient; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 34 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors; 35 import com.android.ondevicepersonalization.services.data.events.EventUrlHelper; 36 import com.android.ondevicepersonalization.services.data.events.EventUrlPayload; 37 import com.android.ondevicepersonalization.services.serviceflow.ServiceFlowOrchestrator; 38 import com.android.ondevicepersonalization.services.serviceflow.ServiceFlowType; 39 40 import com.google.common.util.concurrent.FutureCallback; 41 import com.google.common.util.concurrent.ListeningExecutorService; 42 43 import org.jetbrains.annotations.NotNull; 44 45 import java.io.ByteArrayInputStream; 46 import java.io.InputStream; 47 import java.net.HttpURLConnection; 48 import java.util.Collections; 49 import java.util.Objects; 50 51 public class OdpWebViewClient extends WebViewClient { 52 private static final String TAG = "OdpWebViewClient"; 53 54 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 55 private static final ServiceFlowOrchestrator sSfo = ServiceFlowOrchestrator.getInstance(); 56 57 long mQueryId; 58 private final Injector mInjector; 59 @NonNull 60 private final Context mContext; 61 @NonNull 62 private final ComponentName mService; 63 @Nullable 64 private final RequestLogRecord mLogRecord; 65 66 private static final WebResourceResponse EMPTY_RESPONSE = new WebResourceResponse( 67 /* MimeType= */ null, /* Encoding= */ null, 68 HttpURLConnection.HTTP_NO_CONTENT, "No Content", 69 Collections.emptyMap(), InputStream.nullInputStream()); 70 OdpWebViewClient(Context context, ComponentName service, long queryId, RequestLogRecord logRecord)71 public OdpWebViewClient(Context context, ComponentName service, long queryId, 72 RequestLogRecord logRecord) { 73 this(context, service, queryId, logRecord, new Injector()); 74 } 75 76 @VisibleForTesting OdpWebViewClient(Context context, ComponentName service, long queryId, RequestLogRecord logRecord, Injector injector)77 public OdpWebViewClient(Context context, ComponentName service, long queryId, 78 RequestLogRecord logRecord, Injector injector) { 79 mContext = context; 80 mService = service; 81 mQueryId = queryId; 82 mLogRecord = logRecord; 83 mInjector = injector; 84 } 85 86 @VisibleForTesting 87 public static class Injector { getExecutor()88 ListeningExecutorService getExecutor() { 89 return OnDevicePersonalizationExecutors.getBackgroundExecutor(); 90 } 91 getFutureCallback()92 FutureCallback<EventOutputParcel> getFutureCallback() { 93 return new FutureCallback<>() { 94 @Override 95 public void onSuccess(EventOutputParcel result) {} 96 97 @Override 98 public void onFailure(@NotNull Throwable t) {} 99 }; 100 } 101 openUrl(String landingPage, Context context)102 void openUrl(String landingPage, Context context) { 103 if (landingPage != null) { 104 sLogger.d(TAG + ": Sending intent to open landingPage: " + landingPage); 105 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(landingPage)); 106 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 107 context.startActivity(intent); 108 } 109 } 110 } 111 112 @Override shouldInterceptRequest( @onNull WebView webView, @NonNull WebResourceRequest request)113 public WebResourceResponse shouldInterceptRequest( 114 @NonNull WebView webView, @NonNull WebResourceRequest request) { 115 FutureCallback<EventOutputParcel> callback = mInjector.getFutureCallback(); 116 117 try { 118 if (!EventUrlHelper.isOdpUrl(request.getUrl().toString())) { 119 return null; 120 } 121 122 EventUrlPayload payload = getPayLoadFromRequest(webView, request); 123 124 try { 125 sSfo.schedule(ServiceFlowType.WEB_VIEW_FLOW, mContext, 126 mService, mQueryId, mLogRecord, callback, payload); 127 } catch (Exception e) { 128 sLogger.e(e, TAG + ": shouldInterceptRequest: WebViewFlow failed."); 129 callback.onFailure(e); 130 } 131 132 byte[] responseData = payload.getResponseData(); 133 if (responseData == null || responseData.length == 0) { 134 return EMPTY_RESPONSE; 135 } else { 136 return new WebResourceResponse( 137 payload.getMimeType(), /* Encoding= */ null, 138 HttpURLConnection.HTTP_OK, "OK", 139 Collections.emptyMap(), new ByteArrayInputStream(responseData)); 140 } 141 } catch (Exception e) { 142 sLogger.e(e, TAG + ": shouldInterceptRequest failed."); 143 return null; 144 } 145 } 146 147 @Override shouldOverrideUrlLoading( @onNull WebView webView, @NonNull WebResourceRequest request)148 public boolean shouldOverrideUrlLoading( 149 @NonNull WebView webView, @NonNull WebResourceRequest request) { 150 FutureCallback<EventOutputParcel> callback = mInjector.getFutureCallback(); 151 152 try { 153 if (!EventUrlHelper.isOdpUrl(request.getUrl().toString())) { 154 return false; 155 } 156 157 EventUrlPayload payload = getPayLoadFromRequest(webView, request); 158 159 try { 160 sSfo.schedule(ServiceFlowType.WEB_VIEW_FLOW, mContext, 161 mService, mQueryId, mLogRecord, mInjector.getFutureCallback(), payload); 162 } catch (Exception e) { 163 sLogger.e(e, TAG + ": shouldOverrideUrlLoading: WebViewFlow failed."); 164 callback.onFailure(e); 165 } 166 167 String landingPage = request.getUrl().getQueryParameter( 168 EventUrlHelper.URL_LANDING_PAGE_EVENT_KEY); 169 mInjector.openUrl(landingPage, webView.getContext()); 170 171 return true; 172 } catch (Exception e) { 173 sLogger.e(e, TAG + ": shouldOverrideUrlLoading failed."); 174 return false; 175 } 176 } 177 getPayLoadFromRequest( @onNull WebView webView, @NonNull WebResourceRequest request)178 private EventUrlPayload getPayLoadFromRequest( 179 @NonNull WebView webView, @NonNull WebResourceRequest request) throws Exception { 180 Objects.requireNonNull(webView); 181 Objects.requireNonNull(request); 182 Objects.requireNonNull(request.getUrl()); 183 184 String url = request.getUrl().toString(); 185 186 if (!EventUrlHelper.isOdpUrl(url)) { 187 throw new IllegalArgumentException("Input request does not contain a valid ODP URL."); 188 } 189 190 return EventUrlHelper.getEventFromOdpEventUrl(url); 191 } 192 } 193