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