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