WebViewContentsClientAdapter.java revision 9c5a40623b62737244deb2d04a98830d7ffa8404
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    @Override
291    public void onNewPicture(Picture picture) {
292        if (mPictureListener == null) return;
293        mPictureListener.onNewPicture(mWebView, picture);
294    }
295
296    @Override
297    public void onLoadResource(String url) {
298        mWebViewClient.onLoadResource(mWebView, url);
299    }
300
301    @Override
302    public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) {
303        Message m = mUiThreadHandler.obtainMessage(
304                NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport());
305        return mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m);
306    }
307
308    /**
309     * @see AwContentsClient#onCloseWindow()
310     */
311    /* @Override */
312    public void onCloseWindow() {
313        mWebChromeClient.onCloseWindow(mWebView);
314    }
315
316    /**
317     * @see AwContentsClient#onRequestFocus()
318     */
319    /* @Override */
320    public void onRequestFocus() {
321        mWebChromeClient.onRequestFocus(mWebView);
322    }
323
324    //--------------------------------------------------------------------------------------------
325    //                        Trivial Chrome -> WebViewClient mappings.
326    //--------------------------------------------------------------------------------------------
327
328    /**
329     * @see ContentViewClient#onPageStarted(String)
330     */
331    @Override
332    public void onPageStarted(String url) {
333        //TODO: Can't get the favicon till b/6094807 is fixed.
334        mWebViewClient.onPageStarted(mWebView, url, null);
335    }
336
337    /**
338     * @see ContentViewClient#onPageFinished(String)
339     */
340    @Override
341    public void onPageFinished(String url) {
342        mWebViewClient.onPageFinished(mWebView, url);
343    }
344
345    /**
346     * @see ContentViewClient#onReceivedError(int,String,String)
347     */
348    @Override
349    public void onReceivedError(int errorCode, String description, String failingUrl) {
350        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
351    }
352
353    /**
354     * @see ContentViewClient#onUpdateTitle(String)
355     */
356    @Override
357    public void onUpdateTitle(String title) {
358        mWebChromeClient.onReceivedTitle(mWebView, title);
359    }
360
361
362    /**
363     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
364     */
365    @Override
366    public boolean shouldOverrideKeyEvent(KeyEvent event) {
367        // TODO(joth): The expression here is a workaround for http://b/7697782 :-
368        // 1. The check for system key should be made in AwContents or ContentViewCore,
369        //    before shouldOverrideKeyEvent() is called at all.
370        // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from
371        //    dispatchKeyEvent().
372        return event.isSystem() ||
373            mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
374    }
375
376
377    //--------------------------------------------------------------------------------------------
378    //                 More complicated mappings (including behavior choices)
379    //--------------------------------------------------------------------------------------------
380
381    /**
382     * @see ContentViewClient#onTabCrash()
383     */
384    @Override
385    public void onTabCrash() {
386        // The WebViewClassic implementation used a single process, so any crash would
387        // cause the application to terminate.  WebViewChromium should have the same
388        // behavior as long as we run the renderer in-process. This needs to be revisited
389        // if we change that decision.
390        Log.e(TAG, "Renderer crash reported.");
391        mWebChromeClient.onCloseWindow(mWebView);
392    }
393
394    //--------------------------------------------------------------------------------------------
395    //                                     The TODO section
396    //--------------------------------------------------------------------------------------------
397
398
399    /**
400     * @see ContentViewClient#onImeEvent()
401     */
402    @Override
403    public void onImeEvent() {
404    }
405
406    /**
407     * @see ContentViewClient#onEvaluateJavaScriptResult(int,String)
408     */
409    @Override
410    public void onEvaluateJavaScriptResult(int id, String jsonResult) {
411    }
412
413    /**
414     * @see ContentViewClient#onStartContentIntent(Context, String)
415     * Callback when detecting a click on a content link.
416     */
417    @Override
418    public void onStartContentIntent(Context context, String contentUrl) {
419        mWebViewClient.shouldOverrideUrlLoading(mWebView, contentUrl);
420    }
421
422    private static class SimpleJsResultReceiver implements JsResult.ResultReceiver {
423        private JsResultReceiver mChromeResultReceiver;
424
425        public SimpleJsResultReceiver(JsResultReceiver receiver) {
426            mChromeResultReceiver = receiver;
427        }
428
429        @Override
430        public void onJsResultComplete(JsResult result) {
431            if (result.getResult()) {
432                mChromeResultReceiver.confirm();
433            } else {
434                mChromeResultReceiver.cancel();
435            }
436        }
437    }
438
439    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
440        private JsPromptResultReceiver mChromeResultReceiver;
441        private JsPromptResult mPromptResult;
442
443        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
444            mChromeResultReceiver = receiver;
445            // We hold onto the JsPromptResult here, just to avoid the need to downcast
446            // in onJsResultComplete.
447            mPromptResult = new JsPromptResult(this);
448        }
449
450        public JsPromptResult getPromptResult() {
451            return mPromptResult;
452        }
453
454        @Override
455        public void onJsResultComplete(JsResult result) {
456            if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance");
457            if (mPromptResult.getResult()) {
458                mChromeResultReceiver.confirm(mPromptResult.getStringResult());
459            } else {
460                mChromeResultReceiver.cancel();
461            }
462        }
463    }
464
465    @Override
466    public void onGeolocationPermissionsShowPrompt(String origin,
467            GeolocationPermissions.Callback callback) {
468        mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback);
469    }
470
471    @Override
472    public void onGeolocationPermissionsHidePrompt() {
473        mWebChromeClient.onGeolocationPermissionsHidePrompt();
474    }
475
476    @Override
477    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
478        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
479        mWebChromeClient.onJsAlert(mWebView, url, message, res);
480        // TODO: Handle the case of the client returning false;
481    }
482
483    @Override
484    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
485        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
486        mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res);
487        // TODO: Handle the case of the client returning false;
488    }
489
490    @Override
491    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
492        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
493        mWebChromeClient.onJsConfirm(mWebView, url, message, res);
494        // TODO: Handle the case of the client returning false;
495    }
496
497    @Override
498    public void handleJsPrompt(String url, String message, String defaultValue,
499            JsPromptResultReceiver receiver) {
500        JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult();
501        mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res);
502        // TODO: Handle the case of the client returning false;
503    }
504
505    @Override
506    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
507        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
508                new AwHttpAuthHandlerAdapter(handler), host, realm);
509    }
510
511    @Override
512    public void onFormResubmission(Message dontResend, Message resend) {
513        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
514    }
515
516    @Override
517    public void onDownloadStart(String url,
518                                String userAgent,
519                                String contentDisposition,
520                                String mimeType,
521                                long contentLength) {
522        if (mDownloadListener != null) {
523            mDownloadListener.onDownloadStart(url,
524                                              userAgent,
525                                              contentDisposition,
526                                              mimeType,
527                                              contentLength);
528        }
529    }
530
531    @Override
532    public void onScaleChanged(float oldScale, float newScale) {
533        mWebViewClient.onScaleChanged(mWebView, oldScale, newScale);
534    }
535
536    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
537        private AwHttpAuthHandler mAwHandler;
538
539        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
540            mAwHandler = awHandler;
541        }
542
543        @Override
544        public void proceed(String username, String password) {
545            if (username == null) {
546                username = "";
547            }
548
549            if (password == null) {
550                password = "";
551            }
552            mAwHandler.proceed(username, password);
553        }
554
555        @Override
556        public void cancel() {
557            mAwHandler.cancel();
558        }
559
560        @Override
561        public boolean useHttpAuthUsernamePassword() {
562            return mAwHandler.isFirstAttempt();
563        }
564    }
565}
566