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