WebViewContentsClientAdapter.java revision 251a1c8d0a987b0df24e19904d0c0ef492304286
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    /**
78     * Adapter constructor.
79     *
80     * @param webView the {@link WebView} instance that this adapter is serving.
81     */
82    WebViewContentsClientAdapter(WebView webView) {
83        if (webView == null) {
84            throw new IllegalArgumentException("webView can't be null");
85        }
86
87        mWebView = webView;
88        setWebViewClient(null);
89        setWebChromeClient(null);
90    }
91
92    // WebViewClassic is coded in such a way that even if a null WebViewClient is set,
93    // certain actions take place.
94    // We choose to replicate this behavior by using a NullWebViewClient implementation (also known
95    // as the Null Object pattern) rather than duplicating the WebViewClassic approach in
96    // ContentView.
97    static class NullWebViewClient extends WebViewClient {
98        // The Context that was passed to the WebView by the external client app.
99        private final Context mContext;
100
101        NullWebViewClient(Context context) {
102            mContext = context;
103        }
104
105        @Override
106        public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
107            // TODO: Investigate more and add a test case.
108            // This is a copy of what Clank does. The WebViewCore key handling code and Clank key
109            // handling code differ enough that it's not trivial to figure out how keycodes are
110            // being filtered.
111            int keyCode = event.getKeyCode();
112            if (keyCode == KeyEvent.KEYCODE_MENU ||
113                keyCode == KeyEvent.KEYCODE_HOME ||
114                keyCode == KeyEvent.KEYCODE_BACK ||
115                keyCode == KeyEvent.KEYCODE_CALL ||
116                keyCode == KeyEvent.KEYCODE_ENDCALL ||
117                keyCode == KeyEvent.KEYCODE_POWER ||
118                keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
119                keyCode == KeyEvent.KEYCODE_CAMERA ||
120                keyCode == KeyEvent.KEYCODE_FOCUS ||
121                keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
122                keyCode == KeyEvent.KEYCODE_VOLUME_MUTE ||
123                keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
124                return true;
125            }
126            return false;
127        }
128
129        @Override
130        public boolean shouldOverrideUrlLoading(WebView view, String url) {
131            Intent intent;
132            // Perform generic parsing of the URI to turn it into an Intent.
133            try {
134                intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
135            } catch (URISyntaxException ex) {
136                Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage());
137                return false;
138            }
139            // Sanitize the Intent, ensuring web pages can not bypass browser
140            // security (only access to BROWSABLE activities).
141            intent.addCategory(Intent.CATEGORY_BROWSABLE);
142            intent.setComponent(null);
143            // Pass the package name as application ID so that the intent from the
144            // same application can be opened in the same tab.
145            intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName());
146            try {
147                mContext.startActivity(intent);
148            } catch (ActivityNotFoundException ex) {
149                Log.w(TAG, "No application can handle " + url);
150                return false;
151            }
152            return true;
153        }
154    }
155
156    void setWebViewClient(WebViewClient client) {
157        if (client != null) {
158            mWebViewClient = client;
159        } else {
160            mWebViewClient = new NullWebViewClient(mWebView.getContext());
161        }
162    }
163
164    void setWebChromeClient(WebChromeClient client) {
165        if (client != null) {
166            mWebChromeClient = client;
167        } else {
168            // WebViewClassic doesn't implement any special behavior for a null WebChromeClient.
169            mWebChromeClient = new WebChromeClient();
170        }
171    }
172
173    void setFindListener(WebView.FindListener listener) {
174        mFindListener = listener;
175    }
176
177    void setPictureListener(WebView.PictureListener listener) {
178        mPictureListener = listener;
179    }
180
181    //--------------------------------------------------------------------------------------------
182    //                        Adapter for WebContentsDelegate methods.
183    //--------------------------------------------------------------------------------------------
184
185    /**
186     * @see AwContentsClient#onProgressChanged(int)
187     */
188    @Override
189    public void onProgressChanged(int progress) {
190        mWebChromeClient.onProgressChanged(mWebView, progress);
191    }
192
193    /**
194     * @see AwContentsClient#shouldInterceptRequest(java.lang.String)
195     */
196    @Override
197    public InterceptedRequestData shouldInterceptRequest(String url) {
198        WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, url);
199        if (response == null) return null;
200        return new InterceptedRequestData(
201                response.getMimeType(),
202                response.getEncoding(),
203                response.getData());
204    }
205
206    /**
207     * @see AwContentsClient#shouldIgnoreNavigation(java.lang.String)
208     */
209    @Override
210    public boolean shouldIgnoreNavigation(String url) {
211      return mWebViewClient.shouldOverrideUrlLoading(mWebView, url);
212    }
213
214    /**
215     * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent)
216     */
217    @Override
218    public void onUnhandledKeyEvent(KeyEvent event) {
219        mWebViewClient.onUnhandledKeyEvent(mWebView, event);
220    }
221
222    /**
223     * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage)
224     */
225    @Override
226    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
227        return mWebChromeClient.onConsoleMessage(consoleMessage);
228    }
229
230    /**
231     * @see AwContentsClient#onFindResultReceived(int,int,boolean)
232     */
233    @Override
234    public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
235            boolean isDoneCounting) {
236        if (mFindListener == null) return;
237        mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
238    }
239
240    @Override
241    public void onLoadResource(String url) {
242        mWebViewClient.onLoadResource(mWebView, url);
243    }
244
245    //--------------------------------------------------------------------------------------------
246    //                        Trivial Chrome -> WebViewClient mappings.
247    //--------------------------------------------------------------------------------------------
248
249    /**
250     * @see ContentViewClient#onPageStarted(String)
251     */
252    @Override
253    public void onPageStarted(String url) {
254        //TODO: Can't get the favicon till b/6094807 is fixed.
255        mWebViewClient.onPageStarted(mWebView, url, null);
256    }
257
258    /**
259     * @see ContentViewClient#onPageFinished(String)
260     */
261    @Override
262    public void onPageFinished(String url) {
263        mWebViewClient.onPageFinished(mWebView, url);
264
265        // HACK: Fake a picture listener update, to allow CTS tests to progress.
266        // TODO: Remove when we have real picture listener updates implemented.
267        if (mPictureListener != null) {
268            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
269                @Override
270                public void run() {
271                    UnimplementedWebViewApi.invoke();
272                    if (mPictureListener != null) {
273                        mPictureListener.onNewPicture(mWebView, new Picture());
274                    }
275                }
276            }, 100);
277        }
278    }
279
280    /**
281     * @see ContentViewClient#onReceivedError(int,String,String)
282     */
283    @Override
284    public void onReceivedError(int errorCode, String description, String failingUrl) {
285        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
286    }
287
288    /**
289     * @see ContentViewClient#onUpdateTitle(String)
290     */
291    @Override
292    public void onUpdateTitle(String title) {
293        mWebChromeClient.onReceivedTitle(mWebView, title);
294    }
295
296
297    /**
298     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
299     */
300    @Override
301    public boolean shouldOverrideKeyEvent(KeyEvent event) {
302        return mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
303    }
304
305
306    //--------------------------------------------------------------------------------------------
307    //                 More complicated mappings (including behavior choices)
308    //--------------------------------------------------------------------------------------------
309
310    /**
311     * @see ContentViewClient#onTabCrash()
312     */
313    @Override
314    public void onTabCrash() {
315        // The WebViewClassic implementation used a single process, so any crash would
316        // cause the application to terminate.  WebViewChromium should have the same
317        // behavior as long as we run the renderer in-process. This needs to be revisited
318        // if we change that decision.
319        Log.e(TAG, "Renderer crash reported.");
320        mWebChromeClient.onCloseWindow(mWebView);
321    }
322
323    //--------------------------------------------------------------------------------------------
324    //                                     The TODO section
325    //--------------------------------------------------------------------------------------------
326
327
328    /**
329     * @see ContentViewClient#onImeEvent()
330     */
331    @Override
332    public void onImeEvent() {
333    }
334
335    /**
336     * @see ContentViewClient#onEvaluateJavaScriptResult(int,String)
337     */
338    @Override
339    public void onEvaluateJavaScriptResult(int id, String jsonResult) {
340    }
341
342    /**
343     * @see ContentViewClient#onStartContentIntent(Context, String)
344     */
345    @Override
346    public void onStartContentIntent(Context context, String contentUrl) {
347    }
348
349    private static class SimpleJsResultReceiver implements JsResult.ResultReceiver {
350        private JsResultReceiver mChromeResultReceiver;
351
352        public SimpleJsResultReceiver(JsResultReceiver receiver) {
353            mChromeResultReceiver = receiver;
354        }
355
356        @Override
357        public void onJsResultComplete(JsResult result) {
358            if (result.getResult()) {
359                mChromeResultReceiver.confirm();
360            } else {
361                mChromeResultReceiver.cancel();
362            }
363        }
364    }
365
366    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
367        private JsPromptResultReceiver mChromeResultReceiver;
368        private JsPromptResult mPromptResult;
369
370        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
371            mChromeResultReceiver = receiver;
372            // We hold onto the JsPromptResult here, just to avoid the need to downcast
373            // in onJsResultComplete.
374            mPromptResult = new JsPromptResult(this);
375        }
376
377        public JsPromptResult getPromptResult() {
378            return mPromptResult;
379        }
380
381        @Override
382        public void onJsResultComplete(JsResult result) {
383            if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance");
384            if (mPromptResult.getResult()) {
385                mChromeResultReceiver.confirm(mPromptResult.getStringResult());
386            } else {
387                mChromeResultReceiver.cancel();
388            }
389        }
390    }
391
392    @Override
393    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
394        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
395        mWebChromeClient.onJsAlert(mWebView, url, message, res);
396        // TODO: Handle the case of the client returning false;
397    }
398
399    @Override
400    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
401        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
402        mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res);
403        // TODO: Handle the case of the client returning false;
404    }
405
406    @Override
407    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
408        JsResult res = new JsResult(new SimpleJsResultReceiver(receiver));
409        mWebChromeClient.onJsConfirm(mWebView, url, message, res);
410        // TODO: Handle the case of the client returning false;
411    }
412
413    @Override
414    public void handleJsPrompt(String url, String message, String defaultValue,
415            JsPromptResultReceiver receiver) {
416        JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult();
417        mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res);
418        // TODO: Handle the case of the client returning false;
419    }
420
421    @Override
422    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
423        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
424                new AwHttpAuthHandlerAdapter(handler), host, realm);
425    }
426
427    @Override
428    public void onFormResubmission(Message dontResend, Message resend) {
429        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
430    }
431
432    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
433        private AwHttpAuthHandler mAwHandler;
434
435        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
436            mAwHandler = awHandler;
437        }
438
439        @Override
440        public void proceed(String username, String password) {
441            if (username == null) {
442                username = "";
443            }
444
445            if (password == null) {
446                password = "";
447            }
448            mAwHandler.proceed(username, password);
449        }
450
451        @Override
452        public void cancel() {
453            mAwHandler.cancel();
454        }
455
456        @Override
457        public boolean useHttpAuthUsernamePassword() {
458            // The documentation for this method says:
459            // Gets whether the credentials stored for the current host (i.e. the host
460            // for which {@link WebViewClient#onReceivedHttpAuthRequest} was called)
461            // are suitable for use. Credentials are not suitable if they have
462            // previously been rejected by the server for the current request.
463            // @return whether the credentials are suitable for use
464            //
465            // The CTS tests point out that it always returns true (at odds with
466            // the documentation).
467            // TODO: Decide whether to follow the docs or follow the classic
468            // implementation and update the docs. For now the latter, as it's
469            // easiest.  (though not updating docs until this is resolved).
470            // See b/6204427.
471            return true;
472        }
473    }
474}
475