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 static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertTrue;
24 import static org.mockito.ArgumentMatchers.any;
25 import static org.mockito.Mockito.mock;
26 import static org.mockito.Mockito.spy;
27 import static org.mockito.Mockito.times;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when;
30 
31 import android.adservices.ondevicepersonalization.EventOutputParcel;
32 import android.adservices.ondevicepersonalization.RequestLogRecord;
33 import android.content.ComponentName;
34 import android.content.ContentValues;
35 import android.content.Context;
36 import android.database.Cursor;
37 import android.net.Uri;
38 import android.os.PersistableBundle;
39 import android.util.Log;
40 import android.webkit.WebResourceRequest;
41 import android.webkit.WebResourceResponse;
42 import android.webkit.WebView;
43 import android.webkit.WebViewClient;
44 
45 import androidx.annotation.NonNull;
46 import androidx.test.core.app.ApplicationProvider;
47 
48 import com.android.compatibility.common.util.ShellUtils;
49 import com.android.dx.mockito.inline.extended.ExtendedMockito;
50 import com.android.modules.utils.build.SdkLevel;
51 import com.android.modules.utils.testing.ExtendedMockitoRule;
52 import com.android.ondevicepersonalization.services.Flags;
53 import com.android.ondevicepersonalization.services.FlagsFactory;
54 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
55 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
56 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
57 import com.android.ondevicepersonalization.services.data.events.EventUrlHelper;
58 import com.android.ondevicepersonalization.services.data.events.EventUrlPayload;
59 import com.android.ondevicepersonalization.services.data.events.EventsContract;
60 import com.android.ondevicepersonalization.services.data.events.EventsDao;
61 import com.android.ondevicepersonalization.services.data.events.Query;
62 import com.android.ondevicepersonalization.services.fbs.EventFields;
63 
64 import com.google.common.util.concurrent.FutureCallback;
65 import com.google.common.util.concurrent.ListeningExecutorService;
66 import com.google.common.util.concurrent.MoreExecutors;
67 
68 import org.junit.After;
69 import org.junit.Before;
70 import org.junit.Rule;
71 import org.junit.Test;
72 import org.junit.runner.RunWith;
73 import org.junit.runners.Parameterized;
74 import org.mockito.Spy;
75 import org.mockito.quality.Strictness;
76 
77 import java.net.HttpURLConnection;
78 import java.nio.ByteBuffer;
79 import java.nio.charset.StandardCharsets;
80 import java.util.Arrays;
81 import java.util.Collection;
82 import java.util.Map;
83 import java.util.concurrent.CountDownLatch;
84 
85 @RunWith(Parameterized.class)
86 public class OdpWebViewClientTests {
87     public final String TAG = OdpWebViewClientTests.class.getSimpleName();
88     private static final long QUERY_ID = 1L;
89     private static final String SERVICE_CLASS = "com.test.TestPersonalizationService";
90     private final Context mContext = ApplicationProvider.getApplicationContext();
91     private static final byte[] RESPONSE_BYTES = {'A', 'B'};
92     private EventUrlPayload mTestEventPayload;
93     private final Query mTestQuery = new Query.Builder(
94             1L,
95             "com.app",
96             ComponentName.createRelative(mContext.getPackageName(), SERVICE_CLASS),
97             "AABBCCDD",
98             "query".getBytes(StandardCharsets.UTF_8))
99             .build();
100     private EventsDao mDao;
101     private OnDevicePersonalizationDbHelper mDbHelper;
102     private OdpWebView mWebView;
103     private String mOpenedUrl;
104 
105     private CountDownLatch mLatch;
106 
107     @Parameterized.Parameter(0)
108     public boolean mIsSipFeatureEnabled;
109 
110     private FutureCallback mTestCallback;
111     private boolean mCallbackSuccess;
112     private boolean mCallbackFailure;
113 
114     @Parameterized.Parameters
data()115     public static Collection<Object[]> data() {
116         return Arrays.asList(
117                 new Object[][] {
118                         {true}, {false}
119                 }
120         );
121     }
122 
123     @Spy
124     private Flags mSpyFlags = spy(FlagsFactory.getFlags());
125 
126     @Rule
127     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
128             .mockStatic(FlagsFactory.class)
129             .setStrictness(Strictness.LENIENT)
130             .build();
131 
132     @Before
setup()133     public void setup() throws Exception {
134         PhFlagsTestUtil.setUpDeviceConfigPermissions();
135         mDbHelper = OnDevicePersonalizationDbHelper.getInstanceForTest(mContext);
136         mDao = EventsDao.getInstanceForTest(mContext);
137         // Insert query for FK constraint
138         mDao.insertQuery(mTestQuery);
139         mLatch = new CountDownLatch(1);
140 
141         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
142         when(mSpyFlags.isSharedIsolatedProcessFeatureEnabled())
143                 .thenReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled);
144         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
145 
146         CountDownLatch latch = new CountDownLatch(1);
147         OnDevicePersonalizationExecutors.getHandlerForMainThread().postAtFrontOfQueue(() -> {
148             mWebView = new OdpWebView(mContext);
149             latch.countDown();
150         });
151         latch.await();
152 
153         mTestCallback = new FutureCallback<EventOutputParcel>() {
154             @Override
155             public void onSuccess(EventOutputParcel result) {
156                 mCallbackSuccess = true;
157                 mLatch.countDown();
158             }
159 
160             @Override
161             public void onFailure(@NonNull Throwable t) {
162                 mCallbackFailure = true;
163                 Log.e(TAG, "Callback onFailure called: ", t);
164                 mLatch.countDown();
165             }
166         };
167 
168         mTestEventPayload = new EventUrlPayload(
169                 createEventParameters(), null, null);
170     }
171 
172     @After
cleanup()173     public void cleanup() {
174         mDbHelper.getWritableDatabase().close();
175         mDbHelper.getReadableDatabase().close();
176         mDbHelper.close();
177     }
178 
179     @Test
testValidUrlOverride()180     public void testValidUrlOverride() throws Exception {
181         WebViewClient webViewClient = getWebViewClient();
182         String odpUrl = EventUrlHelper.getEncryptedOdpEventUrl(mTestEventPayload).toString();
183         WebResourceRequest webResourceRequest = new OdpWebResourceRequest(Uri.parse(odpUrl));
184 
185         assertTrue(webViewClient.shouldOverrideUrlLoading(mWebView, webResourceRequest));
186         mLatch.await();
187 
188         assertTrue(mCallbackSuccess);
189         assertEquals(1,
190                 mDbHelper.getReadableDatabase().query(EventsContract.EventsEntry.TABLE_NAME, null,
191                         null, null, null, null, null).getCount());
192     }
193 
194     @Test
testValidUrlWithNoContentIntercept()195     public void testValidUrlWithNoContentIntercept() throws Exception {
196         WebViewClient webViewClient = getWebViewClient();
197         String odpUrl = EventUrlHelper.getEncryptedOdpEventUrl(mTestEventPayload).toString();
198         WebResourceRequest webResourceRequest = new OdpWebResourceRequest(Uri.parse(odpUrl));
199 
200         WebResourceResponse response = webViewClient.shouldInterceptRequest(
201                 mWebView, webResourceRequest);
202         mLatch.await();
203 
204         assertTrue(mCallbackSuccess);
205         assertEquals(HttpURLConnection.HTTP_NO_CONTENT, response.getStatusCode());
206         assertEquals(1,
207                 mDbHelper.getReadableDatabase().query(EventsContract.EventsEntry.TABLE_NAME, null,
208                         null, null, null, null, null).getCount());
209     }
210 
211     @Test
testValidUrlWithResponseDataIntercept()212     public void testValidUrlWithResponseDataIntercept() throws Exception {
213         WebViewClient webViewClient = getWebViewClient();
214         String odpUrl = EventUrlHelper.getEncryptedOdpEventUrl(new EventUrlPayload(
215                 createEventParameters(), RESPONSE_BYTES, "image/gif")).toString();
216         WebResourceRequest webResourceRequest = new OdpWebResourceRequest(Uri.parse(odpUrl));
217 
218         WebResourceResponse response = webViewClient.shouldInterceptRequest(
219                 mWebView, webResourceRequest);
220         mLatch.await();
221 
222         assertTrue(mCallbackSuccess);
223         assertEquals(HttpURLConnection.HTTP_OK, response.getStatusCode());
224         assertEquals("image/gif", response.getMimeType());
225         assertArrayEquals(RESPONSE_BYTES, response.getData().readAllBytes());
226         assertEquals(1,
227                 mDbHelper.getReadableDatabase().query(EventsContract.EventsEntry.TABLE_NAME, null,
228                         null, null, null, null, null).getCount());
229     }
230 
231     @Test
testValidUrlWithRedirect()232     public void testValidUrlWithRedirect() throws Exception {
233         String landingPage = "https://www.google.com";
234         String odpUrl = EventUrlHelper.getEncryptedClickTrackingUrl(
235                 mTestEventPayload, landingPage).toString();
236         WebResourceRequest webResourceRequest = new OdpWebResourceRequest(Uri.parse(odpUrl));
237         WebViewClient webViewClient = getWebViewClient();
238 
239         assertTrue(webViewClient.shouldOverrideUrlLoading(mWebView, webResourceRequest));
240         mLatch.await();
241 
242         assertTrue(mCallbackSuccess);
243         assertEquals(landingPage, mOpenedUrl);
244         assertEquals(1,
245                 mDbHelper.getReadableDatabase().query(EventsContract.EventsEntry.TABLE_NAME, null,
246                         null, null, null, null, null).getCount());
247     }
248 
249     @Test
testValidUrlWithEventMetrics()250     public void testValidUrlWithEventMetrics() throws Exception {
251         WebViewClient webViewClient = getWebViewClient();
252         String odpUrl = EventUrlHelper.getEncryptedOdpEventUrl(mTestEventPayload).toString();
253         WebResourceRequest webResourceRequest = new OdpWebResourceRequest(Uri.parse(odpUrl));
254 
255         assertTrue(webViewClient.shouldOverrideUrlLoading(mWebView, webResourceRequest));
256         mLatch.await();
257 
258         assertTrue(mCallbackSuccess);
259         Cursor result =
260                 mDbHelper.getReadableDatabase().query(
261                     EventsContract.EventsEntry.TABLE_NAME, null,
262                     null, null, null, null, null);
263         assertEquals(1, result.getCount());
264         result.moveToFirst();
265         int dataColumn = result.getColumnIndex("eventData");
266         byte[] data = result.getBlob(dataColumn);
267         EventFields eventFields = EventFields.getRootAsEventFields(ByteBuffer.wrap(data));
268         assertEquals(1, eventFields.data().entriesLength());
269         assertEquals("x", eventFields.data().entries(0).key());
270         assertEquals(10, eventFields.data().entries(0).longValue());
271     }
272 
273     @Test
testNonOdpUrl()274     public void testNonOdpUrl() throws Exception {
275         WebViewClient webViewClient = getWebViewClient();
276         WebResourceRequest webResourceRequest = new OdpWebResourceRequest(
277                 Uri.parse("https://www.google.com"));
278 
279         assertNull(webViewClient.shouldInterceptRequest(mWebView, webResourceRequest));
280         assertFalse(webViewClient.shouldOverrideUrlLoading(mWebView, webResourceRequest));
281     }
282 
283     @Test
testDefaultInjector()284     public void testDefaultInjector() {
285         // Assert constructor using default injector succeeds.
286         new OdpWebViewClient(mContext,
287                 ComponentName.createRelative(mContext.getPackageName(), SERVICE_CLASS), 0,
288                 new RequestLogRecord.Builder().build());
289 
290         Context mockContext = mock(Context.class);
291         OdpWebViewClient.Injector injector = new OdpWebViewClient.Injector();
292         injector.openUrl("https://google.com", mockContext);
293         assertEquals(injector.getExecutor(),
294                 OnDevicePersonalizationExecutors.getBackgroundExecutor());
295         verify(mockContext, times(1)).startActivity(any());
296     }
297 
298     class TestInjector extends OdpWebViewClient.Injector {
getExecutor()299         ListeningExecutorService getExecutor() {
300             return MoreExecutors.newDirectExecutorService();
301         }
302 
getFutureCallback()303         FutureCallback<EventOutputParcel> getFutureCallback() {
304             return mTestCallback;
305         }
306 
openUrl(String url, Context context)307         void openUrl(String url, Context context) {
308             mOpenedUrl = url;
309         }
310     }
311 
getWebViewClient()312     private WebViewClient getWebViewClient() {
313         RequestLogRecord logRecord =
314                 new RequestLogRecord.Builder().addRow(new ContentValues()).build();
315         return getWebViewClient(QUERY_ID, logRecord);
316     }
317 
getWebViewClient(long queryId, RequestLogRecord logRecord)318     private WebViewClient getWebViewClient(long queryId, RequestLogRecord logRecord) {
319         return new OdpWebViewClient(mContext,
320                 ComponentName.createRelative(mContext.getPackageName(), SERVICE_CLASS),
321                 queryId, logRecord, new TestInjector());
322     }
323 
createEventParameters()324     private static PersistableBundle createEventParameters() {
325         PersistableBundle data = new PersistableBundle();
326         data.putLong("x", 10);
327         return data;
328     }
329 
330     static class OdpWebView extends WebView {
331         private String mLastLoadedUrl;
332 
OdpWebView(@onNull Context context)333         OdpWebView(@NonNull Context context) {
334             super(context);
335         }
336 
337         @Override
loadUrl(String url)338         public void loadUrl(String url) {
339             mLastLoadedUrl = url;
340         }
341     }
342 
343     static class OdpWebResourceRequest implements WebResourceRequest {
344         Uri mUri;
345 
OdpWebResourceRequest(Uri uri)346         OdpWebResourceRequest(Uri uri) {
347             this.mUri = uri;
348         }
349 
350         @Override
getUrl()351         public Uri getUrl() {
352             return mUri;
353         }
354 
355         @Override
isForMainFrame()356         public boolean isForMainFrame() {
357             return false;
358         }
359 
360         @Override
isRedirect()361         public boolean isRedirect() {
362             return false;
363         }
364 
365         @Override
hasGesture()366         public boolean hasGesture() {
367             return false;
368         }
369 
370         @Override
getMethod()371         public String getMethod() {
372             return null;
373         }
374 
375         @Override
getRequestHeaders()376         public Map<String, String> getRequestHeaders() {
377             return null;
378         }
379     }
380 }
381