WebViewContentsClientAdapter.java revision 690b7c64c2aaae62680d137fca34c7fac5176ed2
1/*
2 * Copyright (C) 2012 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 com.android.webview.chromium;
18
19import android.content.ActivityNotFoundException;
20import android.content.Context;
21import android.content.Intent;
22import android.graphics.Bitmap;
23import android.graphics.Picture;
24import android.net.http.SslError;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.Message;
28import android.provider.Browser;
29import android.util.Log;
30import android.view.KeyEvent;
31import android.view.View;
32import android.webkit.ConsoleMessage;
33import android.webkit.DownloadListener;
34import android.webkit.GeolocationPermissions;
35import android.webkit.JsPromptResult;
36import android.webkit.JsResult;
37import android.webkit.SslErrorHandler;
38import android.webkit.ValueCallback;
39import android.webkit.WebChromeClient;
40import android.webkit.WebChromeClient.CustomViewCallback;
41import android.webkit.WebResourceResponse;
42import android.webkit.WebView;
43import android.webkit.WebViewClient;
44
45import org.chromium.android_webview.AwContentsClient;
46import org.chromium.android_webview.AwHttpAuthHandler;
47import org.chromium.android_webview.InterceptedRequestData;
48import org.chromium.android_webview.JsPromptResultReceiver;
49import org.chromium.android_webview.JsResultReceiver;
50import org.chromium.content.browser.ContentView;
51import org.chromium.content.browser.ContentViewClient;
52
53import java.net.URISyntaxException;
54
55/**
56 * An adapter class that forwards the callbacks from {@link ContentViewClient}
57 * to the appropriate {@link WebViewClient} or {@link WebChromeClient}.
58 *
59 * An instance of this class is associated with one {@link WebViewChromium}
60 * instance. A WebViewChromium is a WebView implementation provider (that is
61 * android.webkit.WebView delegates all functionality to it) and has exactly
62 * one corresponding {@link ContentView} instance.
63 *
64 * A {@link ContentViewClient} may be shared between multiple {@link ContentView}s,
65 * and hence multiple WebViews. Many WebViewClient methods pass the source
66 * WebView as an argument. This means that we either need to pass the
67 * corresponding ContentView to the corresponding ContentViewClient methods,
68 * or use an instance of ContentViewClientAdapter per WebViewChromium, to
69 * allow the source WebView to be injected by ContentViewClientAdapter. We
70 * choose the latter, because it makes for a cleaner design.
71 */
72public class WebViewContentsClientAdapter extends AwContentsClient {
73    private static final String TAG = "ContentViewClientAdapter";
74    // The WebView instance that this adapter is serving.
75    private final WebView mWebView;
76    // The WebViewClient instance that was passed to WebView.setWebViewClient().
77    private WebViewClient mWebViewClient;
78    // The WebViewClient instance that was passed to WebView.setContentViewClient().
79    private WebChromeClient mWebChromeClient;
80    // The listener receiving find-in-page API results.
81    private WebView.FindListener mFindListener;
82    // The listener receiving notifications of screen updates.
83    private WebView.PictureListener mPictureListener;
84
85    private DownloadListener mDownloadListener;
86
87    private Handler mUiThreadHandler;
88
89    private static final int NEW_WEBVIEW_CREATED = 100;
90
91    /**
92     * Adapter constructor.
93     *
94     * @param webView the {@link WebView} instance that this adapter is serving.
95     */
96    WebViewContentsClientAdapter(WebView webView) {
97        if (webView == null) {
98            throw new IllegalArgumentException("webView can't be null");
99        }
100
101        mWebView = webView;
102        setWebViewClient(null);
103        setWebChromeClient(null);
104
105        mUiThreadHandler = new Handler() {
106
107            @Override
108            public void handleMessage(Message msg) {
109                switch(msg.what) {
110                    case NEW_WEBVIEW_CREATED:
111                        WebView.WebViewTransport t = (WebView.WebViewTransport) msg.obj;
112                        WebView newWebView = t.getWebView();
113                        if (newWebView == null) {
114                            throw new IllegalArgumentException(
115                                    "Must provide a new WebView for the new window.");
116                        }
117                        if (newWebView == mWebView) {
118                            throw new IllegalArgumentException(
119                                    "Parent WebView cannot host it's own popup window. Please " +
120                                    "use WebSettings.setSupportMultipleWindows(false)");
121                        }
122
123                        if (newWebView.copyBackForwardList().getSize() != 0) {
124                            throw new IllegalArgumentException(
125                                    "New WebView for popup window must not have been previously " +
126                                    "navigated.");
127                        }
128
129                        WebViewChromium.completeWindowCreation(mWebView, newWebView);
130                        break;
131                    default:
132                        throw new IllegalStateException();
133                }
134            }
135        };
136
137    }
138
139    // WebViewClassic is coded in such a way that even if a null WebViewClient is set,
140    // certain actions take place.
141    // We choose to replicate this behavior by using a NullWebViewClient implementation (also known
142    // as the Null Object pattern) rather than duplicating the WebViewClassic approach in
143    // ContentView.
144    static class NullWebViewClient extends WebViewClient {
145        @Override
146        public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
147            // TODO: Investigate more and add a test case.
148            // This is a copy of what Clank does. The WebViewCore key handling code and Clank key
149            // handling code differ enough that it's not trivial to figure out how keycodes are
150            // being filtered.
151            int keyCode = event.getKeyCode();
152            if (keyCode == KeyEvent.KEYCODE_MENU ||
153                keyCode == KeyEvent.KEYCODE_HOME ||
154                keyCode == KeyEvent.KEYCODE_BACK ||
155                keyCode == KeyEvent.KEYCODE_CALL ||
156                keyCode == KeyEvent.KEYCODE_ENDCALL ||
157                keyCode == KeyEvent.KEYCODE_POWER ||
158                keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
159                keyCode == KeyEvent.KEYCODE_CAMERA ||
160                keyCode == KeyEvent.KEYCODE_FOCUS ||
161                keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
162                keyCode == KeyEvent.KEYCODE_VOLUME_MUTE ||
163                keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
164                return true;
165            }
166            return false;
167        }
168
169        @Override
170        public boolean shouldOverrideUrlLoading(WebView view, String url) {
171            Intent intent;
172            // Perform generic parsing of the URI to turn it into an Intent.
173            try {
174                intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
175            } catch (URISyntaxException ex) {
176                Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage());
177                return false;
178            }
179            // Sanitize the Intent, ensuring web pages can not bypass browser
180            // security (only access to BROWSABLE activities).
181            intent.addCategory(Intent.CATEGORY_BROWSABLE);
182            intent.setComponent(null);
183            // Pass the package name as application ID so that the intent from the
184            // same application can be opened in the same tab.
185            intent.putExtra(Browser.EXTRA_APPLICATION_ID,
186                    view.getContext().getPackageName());
187            try {
188                view.getContext().startActivity(intent);
189            } catch (ActivityNotFoundException ex) {
190                Log.w(TAG, "No application can handle " + url);
191                return false;
192            }
193            return true;
194        }
195    }
196
197    void setWebViewClient(WebViewClient client) {
198        if (client != null) {
199            mWebViewClient = client;
200        } else {
201            mWebViewClient = new NullWebViewClient();
202        }
203    }
204
205    void setWebChromeClient(WebChromeClient client) {
206        if (client != null) {
207            mWebChromeClient = client;
208        } else {
209            // WebViewClassic doesn't implement any special behavior for a null WebChromeClient.
210            mWebChromeClient = new WebChromeClient();
211        }
212    }
213
214    void setDownloadListener(DownloadListener listener) {
215        mDownloadListener = listener;
216    }
217
218    void setFindListener(WebView.FindListener listener) {
219        mFindListener = listener;
220    }
221
222    void setPictureListener(WebView.PictureListener listener) {
223        mPictureListener = listener;
224    }
225
226    //--------------------------------------------------------------------------------------------
227    //                        Adapter for WebContentsDelegate methods.
228    //--------------------------------------------------------------------------------------------
229
230    /**
231     * @see AwContentsClient#getVisitedHistory
232     */
233    @Override
234    public void getVisitedHistory(ValueCallback<String[]> callback) {
235        mWebChromeClient.getVisitedHistory(callback);
236    }
237
238    /**
239     * @see AwContentsClient#doUpdateVisiteHistory(String, boolean)
240     */
241    @Override
242    public void doUpdateVisitedHistory(String url, boolean isReload) {
243        mWebViewClient.doUpdateVisitedHistory(mWebView, url, isReload);
244    }
245
246    /**
247     * @see AwContentsClient#onProgressChanged(int)
248     */
249    @Override
250    public void onProgressChanged(int progress) {
251        mWebChromeClient.onProgressChanged(mWebView, progress);
252    }
253
254    /**
255     * @see AwContentsClient#shouldInterceptRequest(java.lang.String)
256     */
257    @Override
258    public InterceptedRequestData shouldInterceptRequest(String url) {
259        WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, url);
260        if (response == null) return null;
261        return new InterceptedRequestData(
262                response.getMimeType(),
263                response.getEncoding(),
264                response.getData());
265    }
266
267    /**
268     * @see AwContentsClient#shouldIgnoreNavigation(java.lang.String)
269     */
270    @Override
271    public boolean shouldIgnoreNavigation(String url) {
272      return mWebViewClient.shouldOverrideUrlLoading(mWebView, url);
273    }
274
275    /**
276     * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent)
277     */
278    @Override
279    public void onUnhandledKeyEvent(KeyEvent event) {
280        mWebViewClient.onUnhandledKeyEvent(mWebView, event);
281    }
282
283    /**
284     * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage)
285     */
286    @Override
287    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
288        return mWebChromeClient.onConsoleMessage(consoleMessage);
289    }
290
291    /**
292     * @see AwContentsClient#onFindResultReceived(int,int,boolean)
293     */
294    @Override
295    public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
296            boolean isDoneCounting) {
297        if (mFindListener == null) return;
298        mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
299    }
300
301    /**
302     * @See AwContentsClient#onNewPicture(Picture)
303     */
304    @Override
305    public void onNewPicture(Picture picture) {
306        if (mPictureListener == null) return;
307        mPictureListener.onNewPicture(mWebView, picture);
308    }
309
310    @Override
311    public void onLoadResource(String url) {
312        mWebViewClient.onLoadResource(mWebView, url);
313    }
314
315    @Override
316    public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) {
317        Message m = mUiThreadHandler.obtainMessage(
318                NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport());
319        return mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m);
320    }
321
322    /**
323     * @see AwContentsClient#onCloseWindow()
324     */
325    /* @Override */
326    public void onCloseWindow() {
327        mWebChromeClient.onCloseWindow(mWebView);
328    }
329
330    /**
331     * @see AwContentsClient#onRequestFocus()
332     */
333    /* @Override */
334    public void onRequestFocus() {
335        mWebChromeClient.onRequestFocus(mWebView);
336    }
337
338    /**
339     * @see AwContentsClient#onReceivedTouchIconUrl(String url, boolean precomposed)
340     */
341    @Override
342    public void onReceivedTouchIconUrl(String url, boolean precomposed) {
343        mWebChromeClient.onReceivedTouchIconUrl(mWebView, url, precomposed);
344    }
345
346    /**
347     * @see AwContentsClient#onReceivedIcon(Bitmap bitmap)
348     */
349    @Override
350    public void onReceivedIcon(Bitmap bitmap) {
351        mWebChromeClient.onReceivedIcon(mWebView, bitmap);
352    }
353
354    //--------------------------------------------------------------------------------------------
355    //                        Trivial Chrome -> WebViewClient mappings.
356    //--------------------------------------------------------------------------------------------
357
358    /**
359     * @see ContentViewClient#onPageStarted(String)
360     */
361    @Override
362    public void onPageStarted(String url) {
363        //TODO: Can't get the favicon till b/6094807 is fixed.
364        mWebViewClient.onPageStarted(mWebView, url, null);
365    }
366
367    /**
368     * @see ContentViewClient#onPageFinished(String)
369     */
370    @Override
371    public void onPageFinished(String url) {
372        mWebViewClient.onPageFinished(mWebView, url);
373
374        // See b/8208948
375        // This fakes an onNewPicture callback after onPageFinished to allow
376        // CTS tests to run in an un-flaky manner. This is required as the
377        // path for sending Picture updates in Chromium are decoupled from the
378        // page loading callbacks, i.e. the Chrome compositor may draw our
379        // content and send the Picture before onPageStarted or onPageFinished
380        // are invoked. The CTS harness discards any pictures it receives before
381        // onPageStarted is invoked, so in the case we get the Picture before that and
382        // no further updates after onPageStarted, we'll fail the test by timing
383        // out waiting for a Picture.
384        // To ensure backwards compatibility, we need to defer sending Picture updates
385        // until onPageFinished has been invoked. This work is being done
386        // upstream, and we can revert this hack when it lands.
387        if (mPictureListener != null) {
388            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
389                @Override
390                public void run() {
391                    UnimplementedWebViewApi.invoke();
392                    if (mPictureListener != null) {
393                        mPictureListener.onNewPicture(mWebView, null);
394                    }
395                }
396            }, 100);
397        }
398    }
399
400    /**
401     * @see ContentViewClient#onReceivedError(int,String,String)
402     */
403    @Override
404    public void onReceivedError(int errorCode, String description, String failingUrl) {
405        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
406    }
407
408    /**
409     * @see ContentViewClient#onUpdateTitle(String)
410     */
411    @Override
412    public void onUpdateTitle(String title) {
413        mWebChromeClient.onReceivedTitle(mWebView, title);
414    }
415
416
417    /**
418     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
419     */
420    @Override
421    public boolean shouldOverrideKeyEvent(KeyEvent event) {
422        // TODO(joth): The expression here is a workaround for http://b/7697782 :-
423        // 1. The check for system key should be made in AwContents or ContentViewCore,
424        //    before shouldOverrideKeyEvent() is called at all.
425        // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from
426        //    dispatchKeyEvent().
427        return event.isSystem() ||
428            mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
429    }
430
431
432    //--------------------------------------------------------------------------------------------
433    //                 More complicated mappings (including behavior choices)
434    //--------------------------------------------------------------------------------------------
435
436    /**
437     * @see ContentViewClient#onTabCrash()
438     */
439    @Override
440    public void onTabCrash() {
441        // The WebViewClassic implementation used a single process, so any crash would
442        // cause the application to terminate.  WebViewChromium should have the same
443        // behavior as long as we run the renderer in-process. This needs to be revisited
444        // if we change that decision.
445        Log.e(TAG, "Renderer crash reported.");
446        mWebChromeClient.onCloseWindow(mWebView);
447    }
448
449    //--------------------------------------------------------------------------------------------
450    //                                     The TODO section
451    //--------------------------------------------------------------------------------------------
452
453
454    /**
455     * @see ContentViewClient#onImeEvent()
456     */
457    @Override
458    public void onImeEvent() {
459    }
460
461    /**
462     * @see ContentViewClient#onStartContentIntent(Context, String)
463     * Callback when detecting a click on a content link.
464     */
465    @Override
466    public void onStartContentIntent(Context context, String contentUrl) {
467        mWebViewClient.shouldOverrideUrlLoading(mWebView, contentUrl);
468    }
469
470    private static class SimpleJsResultReceiver implements JsResult.ResultReceiver {
471        private JsResultReceiver mChromeResultReceiver;
472
473        public SimpleJsResultReceiver(JsResultReceiver receiver) {
474            mChromeResultReceiver = receiver;
475        }
476
477        @Override
478        public void onJsResultComplete(JsResult result) {
479            if (result.getResult()) {
480                mChromeResultReceiver.confirm();
481            } else {
482                mChromeResultReceiver.cancel();
483            }
484        }
485    }
486
487    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
488        private JsPromptResultReceiver mChromeResultReceiver;
489        private JsPromptResult mPromptResult;
490
491        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
492            mChromeResultReceiver = receiver;
493            // We hold onto the JsPromptResult here, just to avoid the need to downcast
494            // in onJsResultComplete.
495            mPromptResult = new JsPromptResult(this);
496        }
497
498        public JsPromptResult getPromptResult() {
499            return mPromptResult;
500        }
501
502        @Override
503        public void onJsResultComplete(JsResult result) {
504            if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance");
505            if (mPromptResult.getResult()) {
506                mChromeResultReceiver.confirm(mPromptResult.getStringResult());
507            } else {
508                mChromeResultReceiver.cancel();
509            }
510        }
511    }
512
513    @Override
514    public void onGeolocationPermissionsShowPrompt(String origin,
515            GeolocationPermissions.Callback callback) {
516        mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback);
517    }
518
519    @Override
520    public void onGeolocationPermissionsHidePrompt() {
521        mWebChromeClient.onGeolocationPermissionsHidePrompt();
522    }
523
524    @Override
525    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
526        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
527        mWebChromeClient.onJsAlert(mWebView, url, message, res);
528        // TODO: Handle the case of the client returning false;
529    }
530
531    @Override
532    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
533        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
534        mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res);
535        // TODO: Handle the case of the client returning false;
536    }
537
538    @Override
539    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
540        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
541        mWebChromeClient.onJsConfirm(mWebView, url, message, res);
542        // TODO: Handle the case of the client returning false;
543    }
544
545    @Override
546    public void handleJsPrompt(String url, String message, String defaultValue,
547            JsPromptResultReceiver receiver) {
548        JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult();
549        mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res);
550        // TODO: Handle the case of the client returning false;
551    }
552
553    @Override
554    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
555        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
556                new AwHttpAuthHandlerAdapter(handler), host, realm);
557    }
558
559    @Override
560    public void onReceivedSslError(final ValueCallback<Boolean> callback, SslError error) {
561        SslErrorHandler handler = new SslErrorHandler() {
562            @Override
563            public void proceed() {
564                postProceed(true);
565            }
566            @Override
567            public void cancel() {
568                postProceed(false);
569            }
570            private void postProceed(final boolean proceed) {
571                post(new Runnable() {
572                        @Override
573                        public void run() {
574                            callback.onReceiveValue(proceed);
575                        }
576                    });
577            }
578        };
579        mWebViewClient.onReceivedSslError(mWebView, handler, error);
580    }
581
582    @Override
583    public void onReceivedLoginRequest(String realm, String account, String args) {
584        mWebViewClient.onReceivedLoginRequest(mWebView, realm, account, args);
585    }
586
587    @Override
588    public void onFormResubmission(Message dontResend, Message resend) {
589        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
590    }
591
592    @Override
593    public void onDownloadStart(String url,
594                                String userAgent,
595                                String contentDisposition,
596                                String mimeType,
597                                long contentLength) {
598        if (mDownloadListener != null) {
599            mDownloadListener.onDownloadStart(url,
600                                              userAgent,
601                                              contentDisposition,
602                                              mimeType,
603                                              contentLength);
604        }
605    }
606
607    @Override
608    public void onScaleChangedScaled(float oldScale, float newScale) {
609        mWebViewClient.onScaleChanged(mWebView, oldScale, newScale);
610    }
611
612    @Override
613    public void onShowCustomView(View view,
614            int requestedOrientation, CustomViewCallback cb) {
615        mWebChromeClient.onShowCustomView(view, requestedOrientation, cb);
616    }
617
618    @Override
619    protected View getVideoLoadingProgressView() {
620        return mWebChromeClient.getVideoLoadingProgressView();
621    }
622
623    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
624        private AwHttpAuthHandler mAwHandler;
625
626        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
627            mAwHandler = awHandler;
628        }
629
630        @Override
631        public void proceed(String username, String password) {
632            if (username == null) {
633                username = "";
634            }
635
636            if (password == null) {
637                password = "";
638            }
639            mAwHandler.proceed(username, password);
640        }
641
642        @Override
643        public void cancel() {
644            mAwHandler.cancel();
645        }
646
647        @Override
648        public boolean useHttpAuthUsernamePassword() {
649            return mAwHandler.isFirstAttempt();
650        }
651    }
652}
653