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
17package androidx.webkit;
18
19import android.graphics.Bitmap;
20import android.net.Uri;
21import android.support.test.filters.MediumTest;
22import android.support.test.runner.AndroidJUnit4;
23import android.webkit.ValueCallback;
24import android.webkit.WebResourceRequest;
25import android.webkit.WebResourceResponse;
26import android.webkit.WebView;
27import android.webkit.WebViewClient;
28
29import org.junit.Assert;
30import org.junit.Assume;
31import org.junit.Before;
32import org.junit.Test;
33import org.junit.runner.RunWith;
34
35import java.util.HashMap;
36import java.util.Map;
37import java.util.concurrent.CountDownLatch;
38import java.util.concurrent.TimeUnit;
39
40@MediumTest
41@RunWith(AndroidJUnit4.class)
42public class WebViewClientCompatTest {
43    private WebViewOnUiThread mWebViewOnUiThread;
44
45    private static final long TEST_TIMEOUT = 20000L;
46    private static final String TEST_URL = "http://www.example.com/";
47    private static final String TEST_SAFE_BROWSING_URL =
48            "chrome://safe-browsing/match?type=malware";
49
50    @Before
51    public void setUp() {
52        mWebViewOnUiThread = new WebViewOnUiThread();
53    }
54
55    @Test
56    public void testShouldOverrideUrlLoadingDefault() {
57        // This never calls into chromium, so we don't need to do any feature checks.
58
59        final MockWebViewClient webViewClient = new MockWebViewClient();
60
61        // Create any valid WebResourceRequest, the return values don't matter much.
62        final WebResourceRequest resourceRequest = new WebResourceRequest() {
63            @Override
64            public Uri getUrl() {
65                return Uri.parse(TEST_URL);
66            }
67
68            @Override
69            public boolean isForMainFrame() {
70                return false;
71            }
72
73            @Override
74            public boolean isRedirect() {
75                return false;
76            }
77
78            @Override
79            public boolean hasGesture() {
80                return false;
81            }
82
83            @Override
84            public String getMethod() {
85                return "GET";
86            }
87
88            @Override
89            public Map<String, String> getRequestHeaders() {
90                return new HashMap<String, String>();
91            }
92        };
93
94        Assert.assertFalse(webViewClient.shouldOverrideUrlLoading(
95                mWebViewOnUiThread.getWebViewOnCurrentThread(), resourceRequest));
96    }
97
98    @Test
99    public void testShouldOverrideUrlLoading() throws InterruptedException {
100        Assume.assumeTrue(
101                WebViewFeature.isFeatureSupported(WebViewFeature.SHOULD_OVERRIDE_WITH_REDIRECTS));
102        Assume.assumeTrue(
103                WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT));
104
105        String data = "<html><body>"
106                + "<a href=\"" + TEST_URL + "\" id=\"link\">new page</a>"
107                + "</body></html>";
108        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
109        final CountDownLatch pageFinishedLatch = new CountDownLatch(1);
110        final MockWebViewClient webViewClient = new MockWebViewClient() {
111            @Override
112            public void onPageFinished(WebView view, String url) {
113                super.onPageFinished(view, url);
114                pageFinishedLatch.countDown();
115            }
116        };
117        mWebViewOnUiThread.setWebViewClient(webViewClient);
118        mWebViewOnUiThread.getSettings().setJavaScriptEnabled(true);
119        clickOnLinkUsingJs("link", mWebViewOnUiThread);
120        pageFinishedLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
121        Assert.assertEquals(TEST_URL,
122                webViewClient.getLastShouldOverrideResourceRequest().getUrl().toString());
123
124        WebResourceRequest request = webViewClient.getLastShouldOverrideResourceRequest();
125        Assert.assertNotNull(request);
126        Assert.assertTrue(request.isForMainFrame());
127        Assert.assertFalse(WebResourceRequestCompat.isRedirect(request));
128        Assert.assertFalse(request.hasGesture());
129    }
130
131    private void clickOnLinkUsingJs(final String linkId, WebViewOnUiThread webViewOnUiThread)
132            throws InterruptedException {
133        final CountDownLatch callbackLatch = new CountDownLatch(1);
134        ValueCallback<String> callback = new ValueCallback<String>() {
135            @Override
136            public void onReceiveValue(String value) {
137                callbackLatch.countDown();
138            }
139        };
140        webViewOnUiThread.evaluateJavascript(
141                "document.getElementById('" + linkId + "').click();"
142                        + "console.log('element with id [" + linkId + "] clicked');", callback);
143        Assert.assertTrue(callbackLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
144    }
145
146    @Test
147    public void testOnReceivedError() throws Exception {
148        Assume.assumeTrue(
149                WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR));
150        Assume.assumeTrue(
151                WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE));
152
153        final MockWebViewClient webViewClient = new MockWebViewClient();
154        mWebViewOnUiThread.setWebViewClient(webViewClient);
155
156        String wrongUri = "invalidscheme://some/resource";
157        Assert.assertNull(webViewClient.getOnReceivedResourceError());
158        mWebViewOnUiThread.loadUrlAndWaitForCompletion(wrongUri);
159        Assert.assertNotNull(webViewClient.getOnReceivedResourceError());
160        Assert.assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
161                webViewClient.getOnReceivedResourceError().getErrorCode());
162    }
163
164    @Test
165    public void testOnReceivedErrorForSubresource() throws Exception {
166        Assume.assumeTrue(
167                WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR));
168
169        final MockWebViewClient webViewClient = new MockWebViewClient();
170        mWebViewOnUiThread.setWebViewClient(webViewClient);
171
172        Assert.assertNull(webViewClient.getOnReceivedResourceError());
173        String data = "<html>"
174                + "  <body>"
175                + "    <img src=\"invalidscheme://some/resource\" />"
176                + "  </body>"
177                + "</html>";
178
179        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
180        Assert.assertNotNull(webViewClient.getOnReceivedResourceError());
181        Assert.assertEquals(WebViewClient.ERROR_UNSUPPORTED_SCHEME,
182                webViewClient.getOnReceivedResourceError().getErrorCode());
183    }
184
185    @Test
186    public void testOnSafeBrowsingHitBackToSafety() throws Throwable {
187        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
188        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
189        Assume.assumeTrue(WebViewFeature.isFeatureSupported(
190                WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY));
191        Assume.assumeTrue(
192                WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE));
193
194        final SafeBrowsingBackToSafetyClient backToSafetyWebViewClient =
195                new SafeBrowsingBackToSafetyClient();
196        mWebViewOnUiThread.setWebViewClient(backToSafetyWebViewClient);
197        WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), true);
198
199        // Load any page
200        String data = "<html><body>some safe page</body></html>";
201        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
202        final String originalUrl = mWebViewOnUiThread.getUrl();
203
204        enableSafeBrowsingAndLoadUnsafePage(backToSafetyWebViewClient);
205
206        // Back to safety should produce a network error
207        Assert.assertNotNull(backToSafetyWebViewClient.getOnReceivedResourceError());
208        Assert.assertEquals(WebViewClient.ERROR_UNSAFE_RESOURCE,
209                backToSafetyWebViewClient.getOnReceivedResourceError().getErrorCode());
210
211        // Check that we actually navigated backward
212        Assert.assertEquals(originalUrl, mWebViewOnUiThread.getUrl());
213    }
214
215    @Test
216    public void testOnSafeBrowsingHitProceed() throws Throwable {
217        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_HIT));
218        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE));
219        Assume.assumeTrue(WebViewFeature.isFeatureSupported(
220                WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED));
221
222        final SafeBrowsingProceedClient proceedWebViewClient = new SafeBrowsingProceedClient();
223        mWebViewOnUiThread.setWebViewClient(proceedWebViewClient);
224        WebSettingsCompat.setSafeBrowsingEnabled(mWebViewOnUiThread.getSettings(), true);
225
226        // Load any page
227        String data = "<html><body>some safe page</body></html>";
228        mWebViewOnUiThread.loadDataAndWaitForCompletion(data, "text/html", null);
229
230        enableSafeBrowsingAndLoadUnsafePage(proceedWebViewClient);
231
232        // Check that we actually proceeded
233        Assert.assertEquals(TEST_SAFE_BROWSING_URL, mWebViewOnUiThread.getUrl());
234    }
235
236    private void enableSafeBrowsingAndLoadUnsafePage(SafeBrowsingClient client) throws Throwable {
237        // Note: Safe Browsing depends on user opt-in as well, so we can't assume it's actually
238        // enabled. #getSafeBrowsingEnabled will tell us the true state of whether Safe Browsing is
239        // enabled.
240        boolean deviceSupportsSafeBrowsing =
241                WebSettingsCompat.getSafeBrowsingEnabled(mWebViewOnUiThread.getSettings());
242        Assume.assumeTrue(deviceSupportsSafeBrowsing);
243
244        Assert.assertNull(client.getOnReceivedResourceError());
245        mWebViewOnUiThread.loadUrlAndWaitForCompletion(TEST_SAFE_BROWSING_URL);
246
247        Assert.assertEquals(TEST_SAFE_BROWSING_URL,
248                client.getOnSafeBrowsingHitRequest().getUrl().toString());
249        Assert.assertTrue(client.getOnSafeBrowsingHitRequest().isForMainFrame());
250    }
251
252    @Test
253    public void testOnPageCommitVisibleCalled() throws Exception {
254        Assume.assumeTrue(WebViewFeature.isFeatureSupported(WebViewFeature.VISUAL_STATE_CALLBACK));
255
256        final CountDownLatch callbackLatch = new CountDownLatch(1);
257
258        mWebViewOnUiThread.setWebViewClient(new WebViewClientCompat() {
259            @Override
260            public void onPageCommitVisible(WebView view, String url) {
261                Assert.assertEquals(url, "about:blank");
262                callbackLatch.countDown();
263            }
264        });
265
266        mWebViewOnUiThread.loadUrl("about:blank");
267        Assert.assertTrue(callbackLatch.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS));
268    }
269
270    private class MockWebViewClient extends WebViewOnUiThread.WaitForLoadedClient {
271        private boolean mOnPageStartedCalled;
272        private boolean mOnPageFinishedCalled;
273        private boolean mOnLoadResourceCalled;
274        private WebResourceErrorCompat mOnReceivedResourceError;
275        private WebResourceResponse mOnReceivedHttpError;
276        private WebResourceRequest mLastShouldOverrideResourceRequest;
277
278        MockWebViewClient() {
279            super(mWebViewOnUiThread);
280        }
281
282        public WebResourceErrorCompat getOnReceivedResourceError() {
283            return mOnReceivedResourceError;
284        }
285
286        public WebResourceResponse getOnReceivedHttpError() {
287            return mOnReceivedHttpError;
288        }
289
290        public WebResourceRequest getLastShouldOverrideResourceRequest() {
291            return mLastShouldOverrideResourceRequest;
292        }
293
294        @Override
295        public void onPageStarted(WebView view, String url, Bitmap favicon) {
296            super.onPageStarted(view, url, favicon);
297            mOnPageStartedCalled = true;
298        }
299
300        @Override
301        public void onPageFinished(WebView view, String url) {
302            super.onPageFinished(view, url);
303            Assert.assertTrue(mOnPageStartedCalled);
304            Assert.assertTrue(mOnLoadResourceCalled);
305            mOnPageFinishedCalled = true;
306        }
307
308        @Override
309        public void onLoadResource(WebView view, String url) {
310            super.onLoadResource(view, url);
311            Assert.assertTrue(mOnPageStartedCalled);
312            mOnLoadResourceCalled = true;
313        }
314
315        @Override
316        @SuppressWarnings("deprecation")
317        public void onReceivedError(WebView view, int errorCode,
318                String description, String failingUrl) {
319            // This can be called if a test runs for a WebView which does not support the {@link
320            // WebViewFeature#RECEIVE_WEB_RESOURCE_ERROR} feature.
321        }
322
323        @Override
324        public void onReceivedError(WebView view, WebResourceRequest request,
325                WebResourceErrorCompat error) {
326            mOnReceivedResourceError = error;
327        }
328
329        @Override
330        public void onReceivedHttpError(WebView view, WebResourceRequest request,
331                WebResourceResponse errorResponse) {
332            super.onReceivedHttpError(view, request, errorResponse);
333            mOnReceivedHttpError = errorResponse;
334        }
335
336        @Override
337        @SuppressWarnings("deprecation")
338        public boolean shouldOverrideUrlLoading(WebView view, String url) {
339            // This can be called if a test runs for a WebView which does not support the {@link
340            // WebViewFeature#SHOULD_OVERRIDE_WITH_REDIRECTS} feature.
341            return false;
342        }
343
344        @Override
345        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
346            mLastShouldOverrideResourceRequest = request;
347            return false;
348        }
349    }
350
351    private class SafeBrowsingClient extends MockWebViewClient {
352        private WebResourceRequest mOnSafeBrowsingHitRequest;
353        private int mOnSafeBrowsingHitThreatType;
354
355        public WebResourceRequest getOnSafeBrowsingHitRequest() {
356            return mOnSafeBrowsingHitRequest;
357        }
358
359        public void setOnSafeBrowsingHitRequest(WebResourceRequest request) {
360            mOnSafeBrowsingHitRequest = request;
361        }
362
363        public int getOnSafeBrowsingHitThreatType() {
364            return mOnSafeBrowsingHitThreatType;
365        }
366
367        public void setOnSafeBrowsingHitThreatType(int type) {
368            mOnSafeBrowsingHitThreatType = type;
369        }
370    }
371
372    private class SafeBrowsingBackToSafetyClient extends SafeBrowsingClient {
373        @Override
374        public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
375                int threatType, SafeBrowsingResponseCompat response) {
376            // Immediately go back to safety to return the network error code
377            setOnSafeBrowsingHitRequest(request);
378            setOnSafeBrowsingHitThreatType(threatType);
379            response.backToSafety(/* report */ true);
380        }
381    }
382
383    private class SafeBrowsingProceedClient extends SafeBrowsingClient {
384        @Override
385        public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
386                int threatType, SafeBrowsingResponseCompat response) {
387            // Proceed through Safe Browsing warnings
388            setOnSafeBrowsingHitRequest(request);
389            setOnSafeBrowsingHitThreatType(threatType);
390            response.proceed(/* report */ true);
391        }
392    }
393}
394