1 /* 2 * Copyright 2018 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 androidx.webkit; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assume.assumeTrue; 21 22 import android.support.test.filters.MediumTest; 23 import android.support.test.runner.AndroidJUnit4; 24 import android.webkit.JavascriptInterface; 25 import android.webkit.WebResourceRequest; 26 import android.webkit.WebResourceResponse; 27 import android.webkit.WebView; 28 29 import org.junit.After; 30 import org.junit.Before; 31 import org.junit.Test; 32 import org.junit.runner.RunWith; 33 34 import java.io.ByteArrayInputStream; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.concurrent.Callable; 38 39 @MediumTest 40 @RunWith(AndroidJUnit4.class) 41 public class ServiceWorkerClientCompatTest { 42 43 // The BASE_URL does not matter since the tests will intercept the load, but it should be https 44 // for the Service Worker registration to succeed. 45 private static final String BASE_URL = "https://www.example.com/"; 46 private static final String INDEX_URL = BASE_URL + "index.html"; 47 private static final String SW_URL = BASE_URL + "sw.js"; 48 private static final String FETCH_URL = BASE_URL + "fetch.html"; 49 50 private static final String JS_INTERFACE_NAME = "Android"; 51 private static final int POLLING_TIMEOUT = 10 * 1000; 52 53 // static HTML page always injected instead of the url loaded. 54 private static final String INDEX_RAW_HTML = 55 "<!DOCTYPE html>\n" 56 + "<html>\n" 57 + " <body>\n" 58 + " <script>\n" 59 + " navigator.serviceWorker.register('sw.js').then(function(reg) {\n" 60 + " " + JS_INTERFACE_NAME + ".registrationSuccess();\n" 61 + " }).catch(function(err) {\n" 62 + " console.error(err);\n" 63 + " });\n" 64 + " </script>\n" 65 + " </body>\n" 66 + "</html>\n"; 67 private static final String SW_RAW_HTML = "fetch('fetch.html');"; 68 private static final String SW_UNREGISTER_RAW_JS = 69 "navigator.serviceWorker.getRegistration().then(function(r) {" 70 + " r.unregister().then(function(success) {" 71 + " if (success) " + JS_INTERFACE_NAME + ".unregisterSuccess();" 72 + " else console.error('unregister() was not successful');" 73 + " });" 74 + "}).catch(function(err) {" 75 + " console.error(err);" 76 + "});"; 77 78 private JavascriptStatusReceiver mJavascriptStatusReceiver; 79 private WebViewOnUiThread mOnUiThread; 80 81 // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient, 82 // so this test subclasses the WebViewClient from WebViewOnUiThread. 83 private static class InterceptClient extends WebViewOnUiThread.WaitForLoadedClient { 84 85 InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception { 86 super(webViewOnUiThread); 87 } 88 89 @Override 90 public WebResourceResponse shouldInterceptRequest(WebView view, 91 WebResourceRequest request) { 92 // Only return content for INDEX_URL, deny all other requests. 93 try { 94 if (request.getUrl().toString().equals(INDEX_URL)) { 95 return new WebResourceResponse("text/html", "utf-8", 96 new ByteArrayInputStream(INDEX_RAW_HTML.getBytes("UTF-8"))); 97 } 98 } catch (java.io.UnsupportedEncodingException e) { } 99 return new WebResourceResponse("text/html", "UTF-8", null); 100 } 101 } 102 103 public static class InterceptServiceWorkerClient extends ServiceWorkerClientCompat { 104 private List<WebResourceRequest> mInterceptedRequests = new ArrayList<WebResourceRequest>(); 105 106 @Override 107 public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { 108 // Records intercepted requests and only return content for SW_URL. 109 mInterceptedRequests.add(request); 110 try { 111 if (request.getUrl().toString().equals(SW_URL)) { 112 return new WebResourceResponse("application/javascript", "utf-8", 113 new ByteArrayInputStream(SW_RAW_HTML.getBytes("UTF-8"))); 114 } 115 } catch (java.io.UnsupportedEncodingException e) { } 116 return new WebResourceResponse("text/html", "UTF-8", null); 117 } 118 119 List<WebResourceRequest> getInterceptedRequests() { 120 return mInterceptedRequests; 121 } 122 } 123 124 @Before 125 public void setUp() throws Exception { 126 mOnUiThread = new WebViewOnUiThread(); 127 mOnUiThread.getSettings().setJavaScriptEnabled(true); 128 129 mJavascriptStatusReceiver = new JavascriptStatusReceiver(); 130 mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME); 131 mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread)); 132 } 133 134 @After 135 public void tearDown() throws Exception { 136 if (mOnUiThread != null) { 137 mOnUiThread.cleanUp(); 138 } 139 } 140 141 // Test correct invocation of shouldInterceptRequest for Service Workers. 142 @Test 143 public void testServiceWorkerClientInterceptCallback() throws Exception { 144 assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE)); 145 assumeTrue(WebViewFeature.isFeatureSupported( 146 WebViewFeature.SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST)); 147 148 final InterceptServiceWorkerClient mInterceptServiceWorkerClient = 149 new InterceptServiceWorkerClient(); 150 ServiceWorkerControllerCompat swController = ServiceWorkerControllerCompat.getInstance(); 151 swController.setServiceWorkerClient(mInterceptServiceWorkerClient); 152 153 mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL); 154 155 Callable<Boolean> registrationSuccess = new Callable<Boolean>() { 156 @Override 157 public Boolean call() { 158 return mJavascriptStatusReceiver.mRegistrationSuccess; 159 } 160 }; 161 PollingCheck.check("JS could not register Service Worker", POLLING_TIMEOUT, 162 registrationSuccess); 163 164 Callable<Boolean> receivedRequest = new Callable<Boolean>() { 165 @Override 166 public Boolean call() { 167 return mInterceptServiceWorkerClient.getInterceptedRequests().size() >= 2; 168 } 169 }; 170 PollingCheck.check("Service Worker intercept callbacks not invoked", POLLING_TIMEOUT, 171 receivedRequest); 172 173 List<WebResourceRequest> requests = mInterceptServiceWorkerClient.getInterceptedRequests(); 174 assertEquals(2, requests.size()); 175 assertEquals(SW_URL, requests.get(0).getUrl().toString()); 176 assertEquals(FETCH_URL, requests.get(1).getUrl().toString()); 177 178 // Clean-up, make sure to unregister the Service Worker. 179 mOnUiThread.evaluateJavascript(SW_UNREGISTER_RAW_JS, null); 180 Callable<Boolean> unregisterSuccess = new Callable<Boolean>() { 181 @Override 182 public Boolean call() { 183 return mJavascriptStatusReceiver.mUnregisterSuccess; 184 } 185 }; 186 PollingCheck.check("JS could not unregister Service Worker", POLLING_TIMEOUT, 187 unregisterSuccess); 188 } 189 190 // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to 191 // notify back to Java if the Service Worker registration was successful. 192 public static final class JavascriptStatusReceiver { 193 public volatile boolean mRegistrationSuccess = false; 194 public volatile boolean mUnregisterSuccess = false; 195 196 @JavascriptInterface 197 public void registrationSuccess() { 198 mRegistrationSuccess = true; 199 } 200 201 @JavascriptInterface 202 public void unregisterSuccess() { 203 mUnregisterSuccess = true; 204 } 205 } 206 } 207