WebViewContentsClientAdapter.java revision 1ee9dd0420198429115c3fe8cf0b0502fab9cf62
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    //                        Trivial Chrome -> WebViewClient mappings.
291    //--------------------------------------------------------------------------------------------
292
293    /**
294     * @see ContentViewClient#onPageStarted(String)
295     */
296    @Override
297    public void onPageStarted(String url) {
298        //TODO: Can't get the favicon till b/6094807 is fixed.
299        mWebViewClient.onPageStarted(mWebView, url, null);
300    }
301
302    /**
303     * @see ContentViewClient#onPageFinished(String)
304     */
305    @Override
306    public void onPageFinished(String url) {
307        mWebViewClient.onPageFinished(mWebView, url);
308
309        // HACK: Fake a picture listener update, to allow CTS tests to progress.
310        // TODO: Remove when we have real picture listener updates implemented.
311        if (mPictureListener != null) {
312            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
313                @Override
314                public void run() {
315                    UnimplementedWebViewApi.invoke();
316                    if (mPictureListener != null) {
317                        mPictureListener.onNewPicture(mWebView, new Picture());
318                    }
319                }
320            }, 100);
321        }
322    }
323
324    /**
325     * @see ContentViewClient#onReceivedError(int,String,String)
326     */
327    @Override
328    public void onReceivedError(int errorCode, String description, String failingUrl) {
329        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
330    }
331
332    /**
333     * @see ContentViewClient#onUpdateTitle(String)
334     */
335    @Override
336    public void onUpdateTitle(String title) {
337        mWebChromeClient.onReceivedTitle(mWebView, title);
338    }
339
340
341    /**
342     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
343     */
344    @Override
345    public boolean shouldOverrideKeyEvent(KeyEvent event) {
346        return mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
347    }
348
349
350    //--------------------------------------------------------------------------------------------
351    //                 More complicated mappings (including behavior choices)
352    //--------------------------------------------------------------------------------------------
353
354    /**
355     * @see ContentViewClient#onTabCrash()
356     */
357    @Override
358    public void onTabCrash() {
359        // The WebViewClassic implementation used a single process, so any crash would
360        // cause the application to terminate.  WebViewChromium should have the same
361        // behavior as long as we run the renderer in-process. This needs to be revisited
362        // if we change that decision.
363        Log.e(TAG, "Renderer crash reported.");
364        mWebChromeClient.onCloseWindow(mWebView);
365    }
366
367    //--------------------------------------------------------------------------------------------
368    //                                     The TODO section
369    //--------------------------------------------------------------------------------------------
370
371
372    /**
373     * @see ContentViewClient#onImeEvent()
374     */
375    @Override
376    public void onImeEvent() {
377    }
378
379    /**
380     * @see ContentViewClient#onEvaluateJavaScriptResult(int,String)
381     */
382    @Override
383    public void onEvaluateJavaScriptResult(int id, String jsonResult) {
384    }
385
386    /**
387     * @see ContentViewClient#onStartContentIntent(Context, String)
388     */
389    @Override
390    public void onStartContentIntent(Context context, String contentUrl) {
391    }
392
393    private static class SimpleJsResultReceiver implements JsResult.ResultReceiver {
394        private JsResultReceiver mChromeResultReceiver;
395
396        public SimpleJsResultReceiver(JsResultReceiver receiver) {
397            mChromeResultReceiver = receiver;
398        }
399
400        @Override
401        public void onJsResultComplete(JsResult result) {
402            if (result.getResult()) {
403                mChromeResultReceiver.confirm();
404            } else {
405                mChromeResultReceiver.cancel();
406            }
407        }
408    }
409
410    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
411        private JsPromptResultReceiver mChromeResultReceiver;
412        private JsPromptResult mPromptResult;
413
414        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
415            mChromeResultReceiver = receiver;
416            // We hold onto the JsPromptResult here, just to avoid the need to downcast
417            // in onJsResultComplete.
418            mPromptResult = new JsPromptResult(this);
419        }
420
421        public JsPromptResult getPromptResult() {
422            return mPromptResult;
423        }
424
425        @Override
426        public void onJsResultComplete(JsResult result) {
427            if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance");
428            if (mPromptResult.getResult()) {
429                mChromeResultReceiver.confirm(mPromptResult.getStringResult());
430            } else {
431                mChromeResultReceiver.cancel();
432            }
433        }
434    }
435
436    @Override
437    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
438        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
439        mWebChromeClient.onJsAlert(mWebView, url, message, res);
440        // TODO: Handle the case of the client returning false;
441    }
442
443    @Override
444    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
445        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
446        mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res);
447        // TODO: Handle the case of the client returning false;
448    }
449
450    @Override
451    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
452        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
453        mWebChromeClient.onJsConfirm(mWebView, url, message, res);
454        // TODO: Handle the case of the client returning false;
455    }
456
457    @Override
458    public void handleJsPrompt(String url, String message, String defaultValue,
459            JsPromptResultReceiver receiver) {
460        JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult();
461        mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res);
462        // TODO: Handle the case of the client returning false;
463    }
464
465    @Override
466    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
467        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
468                new AwHttpAuthHandlerAdapter(handler), host, realm);
469    }
470
471    @Override
472    public void onFormResubmission(Message dontResend, Message resend) {
473        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
474    }
475
476    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
477        private AwHttpAuthHandler mAwHandler;
478
479        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
480            mAwHandler = awHandler;
481        }
482
483        @Override
484        public void proceed(String username, String password) {
485            if (username == null) {
486                username = "";
487            }
488
489            if (password == null) {
490                password = "";
491            }
492            mAwHandler.proceed(username, password);
493        }
494
495        @Override
496        public void cancel() {
497            mAwHandler.cancel();
498        }
499
500        @Override
501        public boolean useHttpAuthUsernamePassword() {
502            // The documentation for this method says:
503            // Gets whether the credentials stored for the current host (i.e. the host
504            // for which {@link WebViewClient#onReceivedHttpAuthRequest} was called)
505            // are suitable for use. Credentials are not suitable if they have
506            // previously been rejected by the server for the current request.
507            // @return whether the credentials are suitable for use
508            //
509            // The CTS tests point out that it always returns true (at odds with
510            // the documentation).
511            // TODO: Decide whether to follow the docs or follow the classic
512            // implementation and update the docs. For now the latter, as it's
513            // easiest.  (though not updating docs until this is resolved).
514            // See b/6204427.
515            return true;
516        }
517    }
518}
519