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