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