WebViewContentsClientAdapter.java revision be2a57c401cf1bb65d541b0bef30d71962d22488
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    // TODO: remove this overload, and mark shouldOverrideUrlLoading as @Override
268    public boolean shouldIgnoreNavigation(String url) {
269        return this.shouldOverrideUrlLoading(url);
270    }
271
272    /**
273     * @see AwContentsClient#shouldOverrideUrlLoading(java.lang.String)
274     */
275    //@Override
276    public boolean shouldOverrideUrlLoading(String url) {
277      return mWebViewClient.shouldOverrideUrlLoading(mWebView, url);
278    }
279
280    /**
281     * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent)
282     */
283    @Override
284    public void onUnhandledKeyEvent(KeyEvent event) {
285        mWebViewClient.onUnhandledKeyEvent(mWebView, event);
286    }
287
288    /**
289     * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage)
290     */
291    @Override
292    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
293        return mWebChromeClient.onConsoleMessage(consoleMessage);
294    }
295
296    /**
297     * @see AwContentsClient#onFindResultReceived(int,int,boolean)
298     */
299    @Override
300    public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
301            boolean isDoneCounting) {
302        if (mFindListener == null) return;
303        mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
304    }
305
306    /**
307     * @See AwContentsClient#onNewPicture(Picture)
308     */
309    @Override
310    public void onNewPicture(Picture picture) {
311        if (mPictureListener == null) return;
312        mPictureListener.onNewPicture(mWebView, picture);
313    }
314
315    @Override
316    public void onLoadResource(String url) {
317        mWebViewClient.onLoadResource(mWebView, url);
318    }
319
320    @Override
321    public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) {
322        Message m = mUiThreadHandler.obtainMessage(
323                NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport());
324        return mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m);
325    }
326
327    /**
328     * @see AwContentsClient#onCloseWindow()
329     */
330    /* @Override */
331    public void onCloseWindow() {
332        mWebChromeClient.onCloseWindow(mWebView);
333    }
334
335    /**
336     * @see AwContentsClient#onRequestFocus()
337     */
338    /* @Override */
339    public void onRequestFocus() {
340        mWebChromeClient.onRequestFocus(mWebView);
341    }
342
343    /**
344     * @see AwContentsClient#onReceivedTouchIconUrl(String url, boolean precomposed)
345     */
346    @Override
347    public void onReceivedTouchIconUrl(String url, boolean precomposed) {
348        mWebChromeClient.onReceivedTouchIconUrl(mWebView, url, precomposed);
349    }
350
351    /**
352     * @see AwContentsClient#onReceivedIcon(Bitmap bitmap)
353     */
354    @Override
355    public void onReceivedIcon(Bitmap bitmap) {
356        mWebChromeClient.onReceivedIcon(mWebView, bitmap);
357    }
358
359    //--------------------------------------------------------------------------------------------
360    //                        Trivial Chrome -> WebViewClient mappings.
361    //--------------------------------------------------------------------------------------------
362
363    /**
364     * @see ContentViewClient#onPageStarted(String)
365     */
366    @Override
367    public void onPageStarted(String url) {
368        mWebViewClient.onPageStarted(mWebView, url, mWebView.getFavicon());
369    }
370
371    /**
372     * @see ContentViewClient#onPageFinished(String)
373     */
374    @Override
375    public void onPageFinished(String url) {
376        mWebViewClient.onPageFinished(mWebView, url);
377
378        // See b/8208948
379        // This fakes an onNewPicture callback after onPageFinished to allow
380        // CTS tests to run in an un-flaky manner. This is required as the
381        // path for sending Picture updates in Chromium are decoupled from the
382        // page loading callbacks, i.e. the Chrome compositor may draw our
383        // content and send the Picture before onPageStarted or onPageFinished
384        // are invoked. The CTS harness discards any pictures it receives before
385        // onPageStarted is invoked, so in the case we get the Picture before that and
386        // no further updates after onPageStarted, we'll fail the test by timing
387        // out waiting for a Picture.
388        // To ensure backwards compatibility, we need to defer sending Picture updates
389        // until onPageFinished has been invoked. This work is being done
390        // upstream, and we can revert this hack when it lands.
391        if (mPictureListener != null) {
392            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
393                @Override
394                public void run() {
395                    UnimplementedWebViewApi.invoke();
396                    if (mPictureListener != null) {
397                        mPictureListener.onNewPicture(mWebView, null);
398                    }
399                }
400            }, 100);
401        }
402    }
403
404    /**
405     * @see ContentViewClient#onReceivedError(int,String,String)
406     */
407    @Override
408    public void onReceivedError(int errorCode, String description, String failingUrl) {
409        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
410    }
411
412    // TODO: remove this method, and mark onReceivedTitle as @Override
413    public void onUpdateTitle(String title) {
414        onReceivedTitle(title);
415    }
416
417    /**
418     * @see ContentViewClient#onReceivedTitle(String)
419     */
420    //@Override
421    public void onReceivedTitle(String title) {
422        mWebChromeClient.onReceivedTitle(mWebView, title);
423    }
424
425
426    /**
427     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
428     */
429    @Override
430    public boolean shouldOverrideKeyEvent(KeyEvent event) {
431        // TODO(joth): The expression here is a workaround for http://b/7697782 :-
432        // 1. The check for system key should be made in AwContents or ContentViewCore,
433        //    before shouldOverrideKeyEvent() is called at all.
434        // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from
435        //    dispatchKeyEvent().
436        return event.isSystem() ||
437            mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
438    }
439
440
441    /**
442     * @see ContentViewClient#onStartContentIntent(Context, String)
443     * Callback when detecting a click on a content link.
444     */
445    // TODO: Delete this method when removed from base class.
446    public void onStartContentIntent(Context context, String contentUrl) {
447        mWebViewClient.shouldOverrideUrlLoading(mWebView, contentUrl);
448    }
449
450    private static class SimpleJsResultReceiver implements JsResult.ResultReceiver {
451        private JsResultReceiver mChromeResultReceiver;
452
453        public SimpleJsResultReceiver(JsResultReceiver receiver) {
454            mChromeResultReceiver = receiver;
455        }
456
457        @Override
458        public void onJsResultComplete(JsResult result) {
459            if (result.getResult()) {
460                mChromeResultReceiver.confirm();
461            } else {
462                mChromeResultReceiver.cancel();
463            }
464        }
465    }
466
467    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
468        private JsPromptResultReceiver mChromeResultReceiver;
469        private JsPromptResult mPromptResult;
470
471        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
472            mChromeResultReceiver = receiver;
473            // We hold onto the JsPromptResult here, just to avoid the need to downcast
474            // in onJsResultComplete.
475            mPromptResult = new JsPromptResult(this);
476        }
477
478        public JsPromptResult getPromptResult() {
479            return mPromptResult;
480        }
481
482        @Override
483        public void onJsResultComplete(JsResult result) {
484            if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance");
485            if (mPromptResult.getResult()) {
486                mChromeResultReceiver.confirm(mPromptResult.getStringResult());
487            } else {
488                mChromeResultReceiver.cancel();
489            }
490        }
491    }
492
493    @Override
494    public void onGeolocationPermissionsShowPrompt(String origin,
495            GeolocationPermissions.Callback callback) {
496        mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback);
497    }
498
499    @Override
500    public void onGeolocationPermissionsHidePrompt() {
501        mWebChromeClient.onGeolocationPermissionsHidePrompt();
502    }
503
504    @Override
505    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
506        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
507        mWebChromeClient.onJsAlert(mWebView, url, message, res);
508        // TODO: Handle the case of the client returning false;
509    }
510
511    @Override
512    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
513        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
514        mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res);
515        // TODO: Handle the case of the client returning false;
516    }
517
518    @Override
519    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
520        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
521        mWebChromeClient.onJsConfirm(mWebView, url, message, res);
522        // TODO: Handle the case of the client returning false;
523    }
524
525    @Override
526    public void handleJsPrompt(String url, String message, String defaultValue,
527            JsPromptResultReceiver receiver) {
528        JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult();
529        mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res);
530        // TODO: Handle the case of the client returning false;
531    }
532
533    @Override
534    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
535        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
536                new AwHttpAuthHandlerAdapter(handler), host, realm);
537    }
538
539    @Override
540    public void onReceivedSslError(final ValueCallback<Boolean> callback, SslError error) {
541        SslErrorHandler handler = new SslErrorHandler() {
542            @Override
543            public void proceed() {
544                postProceed(true);
545            }
546            @Override
547            public void cancel() {
548                postProceed(false);
549            }
550            private void postProceed(final boolean proceed) {
551                post(new Runnable() {
552                        @Override
553                        public void run() {
554                            callback.onReceiveValue(proceed);
555                        }
556                    });
557            }
558        };
559        mWebViewClient.onReceivedSslError(mWebView, handler, error);
560    }
561
562    @Override
563    public void onReceivedLoginRequest(String realm, String account, String args) {
564        mWebViewClient.onReceivedLoginRequest(mWebView, realm, account, args);
565    }
566
567    @Override
568    public void onFormResubmission(Message dontResend, Message resend) {
569        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
570    }
571
572    @Override
573    public void onDownloadStart(String url,
574                                String userAgent,
575                                String contentDisposition,
576                                String mimeType,
577                                long contentLength) {
578        if (mDownloadListener != null) {
579            mDownloadListener.onDownloadStart(url,
580                                              userAgent,
581                                              contentDisposition,
582                                              mimeType,
583                                              contentLength);
584        }
585    }
586
587    @Override
588    public void onScaleChangedScaled(float oldScale, float newScale) {
589        mWebViewClient.onScaleChanged(mWebView, oldScale, newScale);
590    }
591
592    @Override
593    public void onShowCustomView(View view,
594            int requestedOrientation, CustomViewCallback cb) {
595        mWebChromeClient.onShowCustomView(view, requestedOrientation, cb);
596    }
597
598    @Override
599    public void onHideCustomView() {
600        mWebChromeClient.onHideCustomView();
601    }
602
603    @Override
604    protected View getVideoLoadingProgressView() {
605        return mWebChromeClient.getVideoLoadingProgressView();
606    }
607
608    @Override
609    public Bitmap getDefaultVideoPoster() {
610        return mWebChromeClient.getDefaultVideoPoster();
611    }
612
613    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
614        private AwHttpAuthHandler mAwHandler;
615
616        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
617            mAwHandler = awHandler;
618        }
619
620        @Override
621        public void proceed(String username, String password) {
622            if (username == null) {
623                username = "";
624            }
625
626            if (password == null) {
627                password = "";
628            }
629            mAwHandler.proceed(username, password);
630        }
631
632        @Override
633        public void cancel() {
634            mAwHandler.cancel();
635        }
636
637        @Override
638        public boolean useHttpAuthUsernamePassword() {
639            return mAwHandler.isFirstAttempt();
640        }
641    }
642}
643