WebViewContentsClientAdapter.java revision d988b42ca3f28aea23dc968b2f7ed79dae868fc6
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.JsPromptResult;
31import android.webkit.JsResult;
32import android.webkit.WebChromeClient;
33import android.webkit.WebResourceResponse;
34import android.webkit.WebView;
35import android.webkit.WebViewClient;
36
37import org.chromium.android_webview.AwContentsClient;
38import org.chromium.android_webview.AwHttpAuthHandler;
39import org.chromium.android_webview.InterceptedRequestData;
40import org.chromium.android_webview.JsPromptResultReceiver;
41import org.chromium.android_webview.JsResultReceiver;
42import org.chromium.content.browser.ContentView;
43import org.chromium.content.browser.ContentViewClient;
44
45import java.net.URISyntaxException;
46
47/**
48 * An adapter class that forwards the callbacks from {@link ContentViewClient}
49 * to the appropriate {@link WebViewClient} or {@link WebChromeClient}.
50 *
51 * An instance of this class is associated with one {@link WebViewChromium}
52 * instance. A WebViewChromium is a WebView implementation provider (that is
53 * android.webkit.WebView delegates all functionality to it) and has exactly
54 * one corresponding {@link ContentView} instance.
55 *
56 * A {@link ContentViewClient} may be shared between multiple {@link ContentView}s,
57 * and hence multiple WebViews. Many WebViewClient methods pass the source
58 * WebView as an argument. This means that we either need to pass the
59 * corresponding ContentView to the corresponding ContentViewClient methods,
60 * or use an instance of ContentViewClientAdapter per WebViewChromium, to
61 * allow the source WebView to be injected by ContentViewClientAdapter. We
62 * choose the latter, because it makes for a cleaner design.
63 */
64public class WebViewContentsClientAdapter extends AwContentsClient {
65    private static final String TAG = "ContentViewClientAdapter";
66    // The WebView instance that this adapter is serving.
67    private final WebView mWebView;
68    // The WebViewClient instance that was passed to WebView.setWebViewClient().
69    private WebViewClient mWebViewClient;
70    // The WebViewClient instance that was passed to WebView.setContentViewClient().
71    private WebChromeClient mWebChromeClient;
72    // The listener receiving find-in-page API results.
73    private WebView.FindListener mFindListener;
74    // The listener receiving notifications of screen updates.
75    private WebView.PictureListener mPictureListener;
76
77    private Handler mUiThreadHandler;
78
79    private static final int NEW_WEBVIEW_CREATED = 100;
80
81    /**
82     * Adapter constructor.
83     *
84     * @param webView the {@link WebView} instance that this adapter is serving.
85     */
86    WebViewContentsClientAdapter(WebView webView) {
87        if (webView == null) {
88            throw new IllegalArgumentException("webView can't be null");
89        }
90
91        mWebView = webView;
92        setWebViewClient(null);
93        setWebChromeClient(null);
94
95        mUiThreadHandler = new Handler() {
96
97            @Override
98            public void handleMessage(Message msg) {
99                switch(msg.what) {
100                    case NEW_WEBVIEW_CREATED:
101                        WebView.WebViewTransport t = (WebView.WebViewTransport) msg.obj;
102                        WebView newWebView = t.getWebView();
103                        if (newWebView == null) {
104                            throw new IllegalArgumentException(
105                                    "Must provide a new WebView for the new window.");
106                        }
107                        if (newWebView == mWebView) {
108                            throw new IllegalArgumentException(
109                                    "Parent WebView cannot host it's own popup window. Please " +
110                                    "use WebSettings.setSupportMultipleWindows(false)");
111                        }
112
113                        if (newWebView.copyBackForwardList().getSize() != 0) {
114                            throw new IllegalArgumentException(
115                                    "New WebView for popup window must not have been previously " +
116                                    "navigated.");
117                        }
118
119                        WebViewChromium.completeWindowCreation(mWebView, newWebView);
120                        break;
121                    default:
122                        throw new IllegalStateException();
123                }
124            }
125        };
126
127    }
128
129    // WebViewClassic is coded in such a way that even if a null WebViewClient is set,
130    // certain actions take place.
131    // We choose to replicate this behavior by using a NullWebViewClient implementation (also known
132    // as the Null Object pattern) rather than duplicating the WebViewClassic approach in
133    // ContentView.
134    static class NullWebViewClient extends WebViewClient {
135        // The Context that was passed to the WebView by the external client app.
136        private final Context mContext;
137
138        NullWebViewClient(Context context) {
139            mContext = context;
140        }
141
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, mContext.getPackageName());
183            try {
184                mContext.startActivity(intent);
185            } catch (ActivityNotFoundException ex) {
186                Log.w(TAG, "No application can handle " + url);
187                return false;
188            }
189            return true;
190        }
191    }
192
193    void setWebViewClient(WebViewClient client) {
194        if (client != null) {
195            mWebViewClient = client;
196        } else {
197            mWebViewClient = new NullWebViewClient(mWebView.getContext());
198        }
199    }
200
201    void setWebChromeClient(WebChromeClient client) {
202        if (client != null) {
203            mWebChromeClient = client;
204        } else {
205            // WebViewClassic doesn't implement any special behavior for a null WebChromeClient.
206            mWebChromeClient = new WebChromeClient();
207        }
208    }
209
210    void setFindListener(WebView.FindListener listener) {
211        mFindListener = listener;
212    }
213
214    void setPictureListener(WebView.PictureListener listener) {
215        mPictureListener = listener;
216    }
217
218    //--------------------------------------------------------------------------------------------
219    //                        Adapter for WebContentsDelegate methods.
220    //--------------------------------------------------------------------------------------------
221
222    /**
223     * @see AwContentsClient#onProgressChanged(int)
224     */
225    @Override
226    public void onProgressChanged(int progress) {
227        mWebChromeClient.onProgressChanged(mWebView, progress);
228    }
229
230    /**
231     * @see AwContentsClient#shouldInterceptRequest(java.lang.String)
232     */
233    @Override
234    public InterceptedRequestData shouldInterceptRequest(String url) {
235        WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, url);
236        if (response == null) return null;
237        return new InterceptedRequestData(
238                response.getMimeType(),
239                response.getEncoding(),
240                response.getData());
241    }
242
243    /**
244     * @see AwContentsClient#shouldIgnoreNavigation(java.lang.String)
245     */
246    @Override
247    public boolean shouldIgnoreNavigation(String url) {
248      return mWebViewClient.shouldOverrideUrlLoading(mWebView, url);
249    }
250
251    /**
252     * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent)
253     */
254    @Override
255    public void onUnhandledKeyEvent(KeyEvent event) {
256        mWebViewClient.onUnhandledKeyEvent(mWebView, event);
257    }
258
259    /**
260     * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage)
261     */
262    @Override
263    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
264        return mWebChromeClient.onConsoleMessage(consoleMessage);
265    }
266
267    /**
268     * @see AwContentsClient#onFindResultReceived(int,int,boolean)
269     */
270    @Override
271    public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
272            boolean isDoneCounting) {
273        if (mFindListener == null) return;
274        mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
275    }
276
277    @Override
278    public void onLoadResource(String url) {
279        mWebViewClient.onLoadResource(mWebView, url);
280    }
281
282    @Override
283    public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) {
284        Message m = mUiThreadHandler.obtainMessage(
285                NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport());
286        return mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m);
287    }
288
289    /**
290     * @see AwContentsClient#onCloseWindow()
291     */
292    /* @Override */
293    public void onCloseWindow() {
294        mWebChromeClient.onCloseWindow(mWebView);
295    }
296
297    //--------------------------------------------------------------------------------------------
298    //                        Trivial Chrome -> WebViewClient mappings.
299    //--------------------------------------------------------------------------------------------
300
301    /**
302     * @see ContentViewClient#onPageStarted(String)
303     */
304    @Override
305    public void onPageStarted(String url) {
306        //TODO: Can't get the favicon till b/6094807 is fixed.
307        mWebViewClient.onPageStarted(mWebView, url, null);
308    }
309
310    /**
311     * @see ContentViewClient#onPageFinished(String)
312     */
313    @Override
314    public void onPageFinished(String url) {
315        mWebViewClient.onPageFinished(mWebView, url);
316
317        // HACK: Fake a picture listener update, to allow CTS tests to progress.
318        // TODO: Remove when we have real picture listener updates implemented.
319        if (mPictureListener != null) {
320            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
321                @Override
322                public void run() {
323                    UnimplementedWebViewApi.invoke();
324                    if (mPictureListener != null) {
325                        mPictureListener.onNewPicture(mWebView, new Picture());
326                    }
327                }
328            }, 100);
329        }
330    }
331
332    /**
333     * @see ContentViewClient#onReceivedError(int,String,String)
334     */
335    @Override
336    public void onReceivedError(int errorCode, String description, String failingUrl) {
337        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
338    }
339
340    /**
341     * @see ContentViewClient#onUpdateTitle(String)
342     */
343    @Override
344    public void onUpdateTitle(String title) {
345        mWebChromeClient.onReceivedTitle(mWebView, title);
346    }
347
348
349    /**
350     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
351     */
352    @Override
353    public boolean shouldOverrideKeyEvent(KeyEvent event) {
354        return mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
355    }
356
357
358    //--------------------------------------------------------------------------------------------
359    //                 More complicated mappings (including behavior choices)
360    //--------------------------------------------------------------------------------------------
361
362    /**
363     * @see ContentViewClient#onTabCrash()
364     */
365    @Override
366    public void onTabCrash() {
367        // The WebViewClassic implementation used a single process, so any crash would
368        // cause the application to terminate.  WebViewChromium should have the same
369        // behavior as long as we run the renderer in-process. This needs to be revisited
370        // if we change that decision.
371        Log.e(TAG, "Renderer crash reported.");
372        mWebChromeClient.onCloseWindow(mWebView);
373    }
374
375    //--------------------------------------------------------------------------------------------
376    //                                     The TODO section
377    //--------------------------------------------------------------------------------------------
378
379
380    /**
381     * @see ContentViewClient#onImeEvent()
382     */
383    @Override
384    public void onImeEvent() {
385    }
386
387    /**
388     * @see ContentViewClient#onEvaluateJavaScriptResult(int,String)
389     */
390    @Override
391    public void onEvaluateJavaScriptResult(int id, String jsonResult) {
392    }
393
394    /**
395     * @see ContentViewClient#onStartContentIntent(Context, String)
396     */
397    @Override
398    public void onStartContentIntent(Context context, String contentUrl) {
399    }
400
401    private static class SimpleJsResultReceiver implements JsResult.ResultReceiver {
402        private JsResultReceiver mChromeResultReceiver;
403
404        public SimpleJsResultReceiver(JsResultReceiver receiver) {
405            mChromeResultReceiver = receiver;
406        }
407
408        @Override
409        public void onJsResultComplete(JsResult result) {
410            if (result.getResult()) {
411                mChromeResultReceiver.confirm();
412            } else {
413                mChromeResultReceiver.cancel();
414            }
415        }
416    }
417
418    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
419        private JsPromptResultReceiver mChromeResultReceiver;
420        private JsPromptResult mPromptResult;
421
422        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
423            mChromeResultReceiver = receiver;
424            // We hold onto the JsPromptResult here, just to avoid the need to downcast
425            // in onJsResultComplete.
426            mPromptResult = new JsPromptResult(this);
427        }
428
429        public JsPromptResult getPromptResult() {
430            return mPromptResult;
431        }
432
433        @Override
434        public void onJsResultComplete(JsResult result) {
435            if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance");
436            if (mPromptResult.getResult()) {
437                mChromeResultReceiver.confirm(mPromptResult.getStringResult());
438            } else {
439                mChromeResultReceiver.cancel();
440            }
441        }
442    }
443
444    @Override
445    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
446        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
447        mWebChromeClient.onJsAlert(mWebView, url, message, res);
448        // TODO: Handle the case of the client returning false;
449    }
450
451    @Override
452    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
453        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
454        mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res);
455        // TODO: Handle the case of the client returning false;
456    }
457
458    @Override
459    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
460        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
461        mWebChromeClient.onJsConfirm(mWebView, url, message, res);
462        // TODO: Handle the case of the client returning false;
463    }
464
465    @Override
466    public void handleJsPrompt(String url, String message, String defaultValue,
467            JsPromptResultReceiver receiver) {
468        JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult();
469        mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res);
470        // TODO: Handle the case of the client returning false;
471    }
472
473    @Override
474    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
475        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
476                new AwHttpAuthHandlerAdapter(handler), host, realm);
477    }
478
479    @Override
480    public void onFormResubmission(Message dontResend, Message resend) {
481        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
482    }
483
484    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
485        private AwHttpAuthHandler mAwHandler;
486
487        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
488            mAwHandler = awHandler;
489        }
490
491        @Override
492        public void proceed(String username, String password) {
493            if (username == null) {
494                username = "";
495            }
496
497            if (password == null) {
498                password = "";
499            }
500            mAwHandler.proceed(username, password);
501        }
502
503        @Override
504        public void cancel() {
505            mAwHandler.cancel();
506        }
507
508        @Override
509        public boolean useHttpAuthUsernamePassword() {
510            // The documentation for this method says:
511            // Gets whether the credentials stored for the current host (i.e. the host
512            // for which {@link WebViewClient#onReceivedHttpAuthRequest} was called)
513            // are suitable for use. Credentials are not suitable if they have
514            // previously been rejected by the server for the current request.
515            // @return whether the credentials are suitable for use
516            //
517            // The CTS tests point out that it always returns true (at odds with
518            // the documentation).
519            // TODO: Decide whether to follow the docs or follow the classic
520            // implementation and update the docs. For now the latter, as it's
521            // easiest.  (though not updating docs until this is resolved).
522            // See b/6204427.
523            return true;
524        }
525    }
526}
527