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