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