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.Bitmap;
23import android.graphics.BitmapFactory;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.Picture;
27import android.net.http.SslError;
28import android.net.Uri;
29import android.os.Build;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.Message;
33import android.provider.Browser;
34import android.util.Log;
35import android.view.KeyEvent;
36import android.view.View;
37import android.webkit.ClientCertRequest;
38import android.webkit.ConsoleMessage;
39import android.webkit.DownloadListener;
40import android.webkit.GeolocationPermissions;
41import android.webkit.JsDialogHelper;
42import android.webkit.JsPromptResult;
43import android.webkit.JsResult;
44import android.webkit.PermissionRequest;
45import android.webkit.SslErrorHandler;
46import android.webkit.ValueCallback;
47import android.webkit.WebChromeClient;
48import android.webkit.WebChromeClient.CustomViewCallback;
49import android.webkit.WebResourceResponse;
50import android.webkit.WebResourceRequest;
51import android.webkit.WebView;
52import android.webkit.WebViewClient;
53
54import com.android.webview.chromium.WebViewDelegateFactory.WebViewDelegate;
55
56import org.chromium.android_webview.AwContentsClient;
57import org.chromium.android_webview.AwContentsClientBridge;
58import org.chromium.android_webview.AwHttpAuthHandler;
59import org.chromium.android_webview.AwWebResourceResponse;
60import org.chromium.android_webview.JsPromptResultReceiver;
61import org.chromium.android_webview.JsResultReceiver;
62import org.chromium.android_webview.permission.AwPermissionRequest;
63import org.chromium.base.ThreadUtils;
64import org.chromium.base.TraceEvent;
65import org.chromium.content.browser.ContentView;
66import org.chromium.content.browser.ContentViewClient;
67
68import java.lang.ref.WeakReference;
69import java.net.URISyntaxException;
70import java.security.Principal;
71import java.security.PrivateKey;
72import java.security.cert.X509Certificate;
73import java.util.ArrayList;
74import java.util.HashMap;
75import java.util.Map;
76import java.util.WeakHashMap;
77
78/**
79 * An adapter class that forwards the callbacks from {@link ContentViewClient}
80 * to the appropriate {@link WebViewClient} or {@link WebChromeClient}.
81 *
82 * An instance of this class is associated with one {@link WebViewChromium}
83 * instance. A WebViewChromium is a WebView implementation provider (that is
84 * android.webkit.WebView delegates all functionality to it) and has exactly
85 * one corresponding {@link ContentView} instance.
86 *
87 * A {@link ContentViewClient} may be shared between multiple {@link ContentView}s,
88 * and hence multiple WebViews. Many WebViewClient methods pass the source
89 * WebView as an argument. This means that we either need to pass the
90 * corresponding ContentView to the corresponding ContentViewClient methods,
91 * or use an instance of ContentViewClientAdapter per WebViewChromium, to
92 * allow the source WebView to be injected by ContentViewClientAdapter. We
93 * choose the latter, because it makes for a cleaner design.
94 */
95public class WebViewContentsClientAdapter extends AwContentsClient {
96    // TAG is chosen for consistency with classic webview tracing.
97    private static final String TAG = "WebViewCallback";
98    // Enables API callback tracing
99    private static final boolean TRACE = false;
100    // The WebView instance that this adapter is serving.
101    private final WebView mWebView;
102    // The WebViewClient instance that was passed to WebView.setWebViewClient().
103    private WebViewClient mWebViewClient;
104    // The WebChromeClient instance that was passed to WebView.setContentViewClient().
105    private WebChromeClient mWebChromeClient;
106    // The listener receiving find-in-page API results.
107    private WebView.FindListener mFindListener;
108    // The listener receiving notifications of screen updates.
109    private WebView.PictureListener mPictureListener;
110
111    private WebViewDelegate mWebViewDelegate;
112
113    private DownloadListener mDownloadListener;
114
115    private Handler mUiThreadHandler;
116
117    private static final int NEW_WEBVIEW_CREATED = 100;
118
119    private WeakHashMap<AwPermissionRequest, WeakReference<PermissionRequestAdapter>>
120            mOngoingPermissionRequests;
121    /**
122     * Adapter constructor.
123     *
124     * @param webView the {@link WebView} instance that this adapter is serving.
125     */
126    WebViewContentsClientAdapter(WebView webView, WebViewDelegate webViewDelegate) {
127        if (webView == null || webViewDelegate == null) {
128            throw new IllegalArgumentException("webView or delegate can't be null");
129        }
130
131        mWebView = webView;
132        mWebViewDelegate = webViewDelegate;
133        setWebViewClient(null);
134
135        mUiThreadHandler = new Handler() {
136
137            @Override
138            public void handleMessage(Message msg) {
139                switch(msg.what) {
140                    case NEW_WEBVIEW_CREATED:
141                        WebView.WebViewTransport t = (WebView.WebViewTransport) msg.obj;
142                        WebView newWebView = t.getWebView();
143                        if (newWebView == mWebView) {
144                            throw new IllegalArgumentException(
145                                    "Parent WebView cannot host it's own popup window. Please " +
146                                    "use WebSettings.setSupportMultipleWindows(false)");
147                        }
148
149                        if (newWebView != null && newWebView.copyBackForwardList().getSize() != 0) {
150                            throw new IllegalArgumentException(
151                                    "New WebView for popup window must not have been previously " +
152                                    "navigated.");
153                        }
154
155                        WebViewChromium.completeWindowCreation(mWebView, newWebView);
156                        break;
157                    default:
158                        throw new IllegalStateException();
159                }
160            }
161        };
162
163    }
164
165    // WebViewClassic is coded in such a way that even if a null WebViewClient is set,
166    // certain actions take place.
167    // We choose to replicate this behavior by using a NullWebViewClient implementation (also known
168    // as the Null Object pattern) rather than duplicating the WebViewClassic approach in
169    // ContentView.
170    static class NullWebViewClient extends WebViewClient {
171        @Override
172        public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
173            // TODO: Investigate more and add a test case.
174            // This is reflecting Clank's behavior.
175            int keyCode = event.getKeyCode();
176            return !ContentViewClient.shouldPropagateKey(keyCode);
177        }
178
179        @Override
180        public boolean shouldOverrideUrlLoading(WebView view, String url) {
181            Intent intent;
182            // Perform generic parsing of the URI to turn it into an Intent.
183            try {
184                intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
185            } catch (URISyntaxException ex) {
186                Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage());
187                return false;
188            }
189            // Sanitize the Intent, ensuring web pages can not bypass browser
190            // security (only access to BROWSABLE activities).
191            intent.addCategory(Intent.CATEGORY_BROWSABLE);
192            intent.setComponent(null);
193            Intent selector = intent.getSelector();
194            if (selector != null) {
195                selector.addCategory(Intent.CATEGORY_BROWSABLE);
196                selector.setComponent(null);
197            }
198            // Pass the package name as application ID so that the intent from the
199            // same application can be opened in the same tab.
200            intent.putExtra(Browser.EXTRA_APPLICATION_ID,
201                    view.getContext().getPackageName());
202            try {
203                view.getContext().startActivity(intent);
204            } catch (ActivityNotFoundException ex) {
205                Log.w(TAG, "No application can handle " + url);
206                return false;
207            }
208            return true;
209        }
210    }
211
212    void setWebViewClient(WebViewClient client) {
213        if (client != null) {
214            mWebViewClient = client;
215        } else {
216            mWebViewClient = new NullWebViewClient();
217        }
218    }
219
220    void setWebChromeClient(WebChromeClient client) {
221        mWebChromeClient = client;
222    }
223
224    void setDownloadListener(DownloadListener listener) {
225        mDownloadListener = listener;
226    }
227
228    void setFindListener(WebView.FindListener listener) {
229        mFindListener = listener;
230    }
231
232    void setPictureListener(WebView.PictureListener listener) {
233        mPictureListener = listener;
234    }
235
236    //--------------------------------------------------------------------------------------------
237    //                        Adapter for all the methods.
238    //--------------------------------------------------------------------------------------------
239
240    /**
241     * @see AwContentsClient#getVisitedHistory
242     */
243    @Override
244    public void getVisitedHistory(ValueCallback<String[]> callback) {
245        TraceEvent.begin();
246        if (mWebChromeClient != null) {
247            if (TRACE) Log.d(TAG, "getVisitedHistory");
248            mWebChromeClient.getVisitedHistory(callback);
249        }
250        TraceEvent.end();
251    }
252
253    /**
254     * @see AwContentsClient#doUpdateVisiteHistory(String, boolean)
255     */
256    @Override
257    public void doUpdateVisitedHistory(String url, boolean isReload) {
258        TraceEvent.begin();
259        if (TRACE) Log.d(TAG, "doUpdateVisitedHistory=" + url + " reload=" + isReload);
260        mWebViewClient.doUpdateVisitedHistory(mWebView, url, isReload);
261        TraceEvent.end();
262    }
263
264    /**
265     * @see AwContentsClient#onProgressChanged(int)
266     */
267    @Override
268    public void onProgressChanged(int progress) {
269        TraceEvent.begin();
270        if (mWebChromeClient != null) {
271            if (TRACE) Log.d(TAG, "onProgressChanged=" + progress);
272            mWebChromeClient.onProgressChanged(mWebView, progress);
273        }
274        TraceEvent.end();
275    }
276
277    private static class WebResourceRequestImpl implements WebResourceRequest {
278        private final ShouldInterceptRequestParams mParams;
279
280        public WebResourceRequestImpl(ShouldInterceptRequestParams params) {
281            mParams = params;
282        }
283
284        @Override
285        public Uri getUrl() {
286            return Uri.parse(mParams.url);
287        }
288
289        @Override
290        public boolean isForMainFrame() {
291            return mParams.isMainFrame;
292        }
293
294        @Override
295        public boolean hasGesture() {
296            return mParams.hasUserGesture;
297        }
298
299        @Override
300        public String getMethod() {
301            return mParams.method;
302        }
303
304        @Override
305        public Map<String, String> getRequestHeaders() {
306            return mParams.requestHeaders;
307        }
308    }
309
310    /**
311     * @see AwContentsClient#shouldInterceptRequest(java.lang.String)
312     */
313    @Override
314    public AwWebResourceResponse shouldInterceptRequest(ShouldInterceptRequestParams params) {
315        TraceEvent.begin();
316        if (TRACE) Log.d(TAG, "shouldInterceptRequest=" + params.url);
317        WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView,
318                new WebResourceRequestImpl(params));
319        TraceEvent.end();
320        if (response == null) return null;
321
322        // AwWebResourceResponse should support null headers. b/16332774.
323        Map<String, String> responseHeaders = response.getResponseHeaders();
324        if (responseHeaders == null)
325            responseHeaders = new HashMap<String, String>();
326
327        return new AwWebResourceResponse(
328                response.getMimeType(),
329                response.getEncoding(),
330                response.getData(),
331                response.getStatusCode(),
332                response.getReasonPhrase(),
333                responseHeaders);
334    }
335
336    /**
337     * @see AwContentsClient#shouldOverrideUrlLoading(java.lang.String)
338     */
339    @Override
340    public boolean shouldOverrideUrlLoading(String url) {
341        TraceEvent.begin();
342        if (TRACE) Log.d(TAG, "shouldOverrideUrlLoading=" + url);
343        boolean result = mWebViewClient.shouldOverrideUrlLoading(mWebView, url);
344        TraceEvent.end();
345        return result;
346    }
347
348    /**
349     * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent)
350     */
351    @Override
352    public void onUnhandledKeyEvent(KeyEvent event) {
353        TraceEvent.begin();
354        if (TRACE) Log.d(TAG, "onUnhandledKeyEvent");
355        mWebViewClient.onUnhandledKeyEvent(mWebView, event);
356        TraceEvent.end();
357    }
358
359    /**
360     * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage)
361     */
362    @Override
363    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
364        TraceEvent.begin();
365        boolean result;
366        if (mWebChromeClient != null) {
367            if (TRACE) Log.d(TAG, "onConsoleMessage: " + consoleMessage.message());
368            result = mWebChromeClient.onConsoleMessage(consoleMessage);
369            String message = consoleMessage.message();
370            if (result && message != null && message.startsWith("[blocked]")) {
371                Log.e(TAG, "Blocked URL: " + message);
372            }
373        } else {
374            result = false;
375        }
376        TraceEvent.end();
377        return result;
378    }
379
380    /**
381     * @see AwContentsClient#onFindResultReceived(int,int,boolean)
382     */
383    @Override
384    public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
385            boolean isDoneCounting) {
386        if (mFindListener == null) return;
387        TraceEvent.begin();
388        if (TRACE) Log.d(TAG, "onFindResultReceived");
389        mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting);
390        TraceEvent.end();
391    }
392
393    /**
394     * @See AwContentsClient#onNewPicture(Picture)
395     */
396    @Override
397    public void onNewPicture(Picture picture) {
398        if (mPictureListener == null) return;
399        TraceEvent.begin();
400        if (TRACE) Log.d(TAG, "onNewPicture");
401        mPictureListener.onNewPicture(mWebView, picture);
402        TraceEvent.end();
403    }
404
405    @Override
406    public void onLoadResource(String url) {
407        TraceEvent.begin();
408        if (TRACE) Log.d(TAG, "onLoadResource=" + url);
409        mWebViewClient.onLoadResource(mWebView, url);
410        TraceEvent.end();
411    }
412
413    @Override
414    public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) {
415        Message m = mUiThreadHandler.obtainMessage(
416                NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport());
417        TraceEvent.begin();
418        boolean result;
419        if (mWebChromeClient != null) {
420            if (TRACE) Log.d(TAG, "onCreateWindow");
421            result = mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m);
422        } else {
423            result = false;
424        }
425        TraceEvent.end();
426        return result;
427    }
428
429    /**
430     * @see AwContentsClient#onCloseWindow()
431     */
432    @Override
433    public void onCloseWindow() {
434        TraceEvent.begin();
435        if (mWebChromeClient != null) {
436            if (TRACE) Log.d(TAG, "onCloseWindow");
437            mWebChromeClient.onCloseWindow(mWebView);
438        }
439        TraceEvent.end();
440    }
441
442    /**
443     * @see AwContentsClient#onRequestFocus()
444     */
445    @Override
446    public void onRequestFocus() {
447        TraceEvent.begin();
448        if (mWebChromeClient != null) {
449            if (TRACE) Log.d(TAG, "onRequestFocus");
450            mWebChromeClient.onRequestFocus(mWebView);
451        }
452        TraceEvent.end();
453    }
454
455    /**
456     * @see AwContentsClient#onReceivedTouchIconUrl(String url, boolean precomposed)
457     */
458    @Override
459    public void onReceivedTouchIconUrl(String url, boolean precomposed) {
460        TraceEvent.begin();
461        if (mWebChromeClient != null) {
462            if (TRACE) Log.d(TAG, "onReceivedTouchIconUrl=" + url);
463            mWebChromeClient.onReceivedTouchIconUrl(mWebView, url, precomposed);
464        }
465        TraceEvent.end();
466    }
467
468    /**
469     * @see AwContentsClient#onReceivedIcon(Bitmap bitmap)
470     */
471    @Override
472    public void onReceivedIcon(Bitmap bitmap) {
473        TraceEvent.begin();
474        if (mWebChromeClient != null) {
475            if (TRACE) Log.d(TAG, "onReceivedIcon");
476            mWebChromeClient.onReceivedIcon(mWebView, bitmap);
477        }
478        TraceEvent.end();
479    }
480
481    /**
482     * @see ContentViewClient#onPageStarted(String)
483     */
484    @Override
485    public void onPageStarted(String url) {
486        TraceEvent.begin();
487        if (TRACE) Log.d(TAG, "onPageStarted=" + url);
488        mWebViewClient.onPageStarted(mWebView, url, mWebView.getFavicon());
489        TraceEvent.end();
490    }
491
492    /**
493     * @see ContentViewClient#onPageFinished(String)
494     */
495    @Override
496    public void onPageFinished(String url) {
497        TraceEvent.begin();
498        if (TRACE) Log.d(TAG, "onPageFinished=" + url);
499        mWebViewClient.onPageFinished(mWebView, url);
500        TraceEvent.end();
501
502        // See b/8208948
503        // This fakes an onNewPicture callback after onPageFinished to allow
504        // CTS tests to run in an un-flaky manner. This is required as the
505        // path for sending Picture updates in Chromium are decoupled from the
506        // page loading callbacks, i.e. the Chrome compositor may draw our
507        // content and send the Picture before onPageStarted or onPageFinished
508        // are invoked. The CTS harness discards any pictures it receives before
509        // onPageStarted is invoked, so in the case we get the Picture before that and
510        // no further updates after onPageStarted, we'll fail the test by timing
511        // out waiting for a Picture.
512        // To ensure backwards compatibility, we need to defer sending Picture updates
513        // until onPageFinished has been invoked. This work is being done
514        // upstream, and we can revert this hack when it lands.
515        if (mPictureListener != null) {
516            ThreadUtils.postOnUiThreadDelayed(new Runnable() {
517                @Override
518                public void run() {
519                    UnimplementedWebViewApi.invoke();
520                    if (mPictureListener != null) {
521                        if (TRACE) Log.d(TAG, "onPageFinished-fake");
522                        mPictureListener.onNewPicture(mWebView, new Picture());
523                    }
524                }
525            }, 100);
526        }
527    }
528
529    /**
530     * @see ContentViewClient#onReceivedError(int,String,String)
531     */
532    @Override
533    public void onReceivedError(int errorCode, String description, String failingUrl) {
534        if (description == null || description.isEmpty()) {
535            // ErrorStrings is @hidden, so we can't do this in AwContents.
536            // Normally the net/ layer will set a valid description, but for synthesized callbacks
537            // (like in the case for intercepted requests) AwContents will pass in null.
538            description = mWebViewDelegate.getErrorString(mWebView.getContext(), errorCode);
539        }
540        TraceEvent.begin();
541        if (TRACE) Log.d(TAG, "onReceivedError=" + failingUrl);
542        mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl);
543        TraceEvent.end();
544    }
545
546    /**
547     * @see ContentViewClient#onReceivedTitle(String)
548     */
549    @Override
550    public void onReceivedTitle(String title) {
551        TraceEvent.begin();
552        if (mWebChromeClient != null) {
553            if (TRACE) Log.d(TAG, "onReceivedTitle");
554            mWebChromeClient.onReceivedTitle(mWebView, title);
555        }
556        TraceEvent.end();
557    }
558
559
560    /**
561     * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent)
562     */
563    @Override
564    public boolean shouldOverrideKeyEvent(KeyEvent event) {
565        // The check below is reflecting Clank's behavior and is a workaround for http://b/7697782.
566        // 1. The check for system key should be made in AwContents or ContentViewCore, before
567        //    shouldOverrideKeyEvent() is called at all.
568        // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from
569        //    dispatchKeyEvent().
570        if (!ContentViewClient.shouldPropagateKey(event.getKeyCode())) return true;
571        TraceEvent.begin();
572        if (TRACE) Log.d(TAG, "shouldOverrideKeyEvent");
573        boolean result = mWebViewClient.shouldOverrideKeyEvent(mWebView, event);
574        TraceEvent.end();
575        return result;
576    }
577
578
579    /**
580     * @see ContentViewClient#onStartContentIntent(Context, String)
581     * Callback when detecting a click on a content link.
582     */
583    // TODO: Delete this method when removed from base class.
584    public void onStartContentIntent(Context context, String contentUrl) {
585        TraceEvent.begin();
586        if (TRACE) Log.d(TAG, "shouldOverrideUrlLoading=" + contentUrl);
587        mWebViewClient.shouldOverrideUrlLoading(mWebView, contentUrl);
588        TraceEvent.end();
589    }
590
591    @Override
592    public void onGeolocationPermissionsShowPrompt(String origin,
593            GeolocationPermissions.Callback callback) {
594        TraceEvent.begin();
595        if (mWebChromeClient != null) {
596            if (TRACE) Log.d(TAG, "onGeolocationPermissionsShowPrompt");
597            mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback);
598        }
599        TraceEvent.end();
600    }
601
602    @Override
603    public void onGeolocationPermissionsHidePrompt() {
604        TraceEvent.begin();
605        if (mWebChromeClient != null) {
606            if (TRACE) Log.d(TAG, "onGeolocationPermissionsHidePrompt");
607            mWebChromeClient.onGeolocationPermissionsHidePrompt();
608        }
609        TraceEvent.end();
610    }
611
612    @Override
613    public void onPermissionRequest(AwPermissionRequest permissionRequest) {
614        TraceEvent.begin();
615        if (mWebChromeClient != null) {
616            if (TRACE) Log.d(TAG, "onPermissionRequest");
617            if (mOngoingPermissionRequests == null) {
618                mOngoingPermissionRequests =
619                    new WeakHashMap<AwPermissionRequest, WeakReference<PermissionRequestAdapter>>();
620            }
621            PermissionRequestAdapter adapter = new PermissionRequestAdapter(permissionRequest);
622            mOngoingPermissionRequests.put(
623                    permissionRequest, new WeakReference<PermissionRequestAdapter>(adapter));
624            mWebChromeClient.onPermissionRequest(adapter);
625        } else {
626            // By default, we deny the permission.
627            permissionRequest.deny();
628        }
629        TraceEvent.end();
630    }
631
632    @Override
633    public void onPermissionRequestCanceled(AwPermissionRequest permissionRequest) {
634        TraceEvent.begin();
635        if (mWebChromeClient != null && mOngoingPermissionRequests != null) {
636            if (TRACE) Log.d(TAG, "onPermissionRequestCanceled");
637            WeakReference<PermissionRequestAdapter> weakRef =
638                    mOngoingPermissionRequests.get(permissionRequest);
639            // We don't hold strong reference to PermissionRequestAdpater and don't expect the
640            // user only holds weak reference to it either, if so, user has no way to call
641            // grant()/deny(), and no need to be notified the cancellation of request.
642            if (weakRef != null) {
643                PermissionRequestAdapter adapter = weakRef.get();
644                if (adapter != null) mWebChromeClient.onPermissionRequestCanceled(adapter);
645            }
646        }
647        TraceEvent.end();
648    }
649
650    private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver {
651        private JsPromptResultReceiver mChromePromptResultReceiver;
652        private JsResultReceiver mChromeResultReceiver;
653        // We hold onto the JsPromptResult here, just to avoid the need to downcast
654        // in onJsResultComplete.
655        private final JsPromptResult mPromptResult = new JsPromptResult(this);
656
657        public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) {
658            mChromePromptResultReceiver = receiver;
659        }
660
661        public JsPromptResultReceiverAdapter(JsResultReceiver receiver) {
662            mChromeResultReceiver = receiver;
663        }
664
665        public JsPromptResult getPromptResult() {
666            return mPromptResult;
667        }
668
669        @Override
670        public void onJsResultComplete(JsResult result) {
671            if (mChromePromptResultReceiver != null) {
672                if (mPromptResult.getResult()) {
673                    mChromePromptResultReceiver.confirm(mPromptResult.getStringResult());
674                } else {
675                    mChromePromptResultReceiver.cancel();
676                }
677            } else {
678                if (mPromptResult.getResult()) {
679                    mChromeResultReceiver.confirm();
680                } else {
681                    mChromeResultReceiver.cancel();
682                }
683            }
684        }
685    }
686
687    @Override
688    public void handleJsAlert(String url, String message, JsResultReceiver receiver) {
689        TraceEvent.begin();
690        if (mWebChromeClient != null) {
691            final JsPromptResult res =
692                    new JsPromptResultReceiverAdapter(receiver).getPromptResult();
693            if (TRACE) Log.d(TAG, "onJsAlert");
694            if (!mWebChromeClient.onJsAlert(mWebView, url, message, res)) {
695                new JsDialogHelper(res, JsDialogHelper.ALERT, null, message, url)
696                        .showDialog(mWebView.getContext());
697            }
698        } else {
699            receiver.cancel();
700        }
701        TraceEvent.end();
702    }
703
704    @Override
705    public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) {
706        TraceEvent.begin();
707        if (mWebChromeClient != null) {
708            final JsPromptResult res =
709                    new JsPromptResultReceiverAdapter(receiver).getPromptResult();
710            if (TRACE) Log.d(TAG, "onJsBeforeUnload");
711            if (!mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res)) {
712                new JsDialogHelper(res, JsDialogHelper.UNLOAD, null, message, url)
713                        .showDialog(mWebView.getContext());
714            }
715        } else {
716            receiver.cancel();
717        }
718        TraceEvent.end();
719    }
720
721    @Override
722    public void handleJsConfirm(String url, String message, JsResultReceiver receiver) {
723        TraceEvent.begin();
724        if (mWebChromeClient != null) {
725            final JsPromptResult res =
726                    new JsPromptResultReceiverAdapter(receiver).getPromptResult();
727            if (TRACE) Log.d(TAG, "onJsConfirm");
728            if (!mWebChromeClient.onJsConfirm(mWebView, url, message, res)) {
729                new JsDialogHelper(res, JsDialogHelper.CONFIRM, null, message, url)
730                        .showDialog(mWebView.getContext());
731            }
732        } else {
733            receiver.cancel();
734        }
735        TraceEvent.end();
736    }
737
738    @Override
739    public void handleJsPrompt(String url, String message, String defaultValue,
740            JsPromptResultReceiver receiver) {
741        TraceEvent.begin();
742        if (mWebChromeClient != null) {
743            final JsPromptResult res =
744                    new JsPromptResultReceiverAdapter(receiver).getPromptResult();
745            if (TRACE) Log.d(TAG, "onJsPrompt");
746            if (!mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res)) {
747                new JsDialogHelper(res, JsDialogHelper.PROMPT, defaultValue, message, url)
748                        .showDialog(mWebView.getContext());
749            }
750        } else {
751            receiver.cancel();
752        }
753        TraceEvent.end();
754    }
755
756    @Override
757    public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
758        TraceEvent.begin();
759        if (TRACE) Log.d(TAG, "onReceivedHttpAuthRequest=" + host);
760        mWebViewClient.onReceivedHttpAuthRequest(mWebView,
761                new AwHttpAuthHandlerAdapter(handler), host, realm);
762        TraceEvent.end();
763    }
764
765    @Override
766    public void onReceivedSslError(final ValueCallback<Boolean> callback, SslError error) {
767        SslErrorHandler handler = new SslErrorHandler() {
768            @Override
769            public void proceed() {
770                callback.onReceiveValue(true);
771            }
772            @Override
773            public void cancel() {
774                callback.onReceiveValue(false);
775            }
776        };
777        TraceEvent.begin();
778        if (TRACE) Log.d(TAG, "onReceivedSslError");
779        mWebViewClient.onReceivedSslError(mWebView, handler, error);
780        TraceEvent.end();
781    }
782
783    private static class ClientCertRequestImpl extends ClientCertRequest {
784
785        final private AwContentsClientBridge.ClientCertificateRequestCallback mCallback;
786        final private String[] mKeyTypes;
787        final private Principal[] mPrincipals;
788        final private String mHost;
789        final private int mPort;
790
791        public ClientCertRequestImpl(
792                AwContentsClientBridge.ClientCertificateRequestCallback callback,
793                String[] keyTypes, Principal[] principals, String host, int port) {
794            mCallback = callback;
795            mKeyTypes = keyTypes;
796            mPrincipals = principals;
797            mHost = host;
798            mPort = port;
799        }
800
801        @Override
802        public String[] getKeyTypes() {
803            // This is already a copy of native argument, so return directly.
804            return mKeyTypes;
805        }
806
807        @Override
808        public Principal[] getPrincipals() {
809            // This is already a copy of native argument, so return directly.
810            return mPrincipals;
811        }
812
813        @Override
814        public String getHost() {
815            return mHost;
816        }
817
818        @Override
819        public int getPort() {
820            return mPort;
821        }
822
823        @Override
824        public void proceed(final PrivateKey privateKey, final X509Certificate[] chain) {
825            mCallback.proceed(privateKey, chain);
826        }
827
828        @Override
829        public void ignore() {
830            mCallback.ignore();
831        }
832
833        @Override
834        public void cancel() {
835            mCallback.cancel();
836        }
837    }
838
839    @Override
840    public void onReceivedClientCertRequest(
841            AwContentsClientBridge.ClientCertificateRequestCallback callback,
842            String[] keyTypes, Principal[] principals, String host, int port) {
843        if (TRACE) Log.d(TAG, "onReceivedClientCertRequest");
844        TraceEvent.begin();
845        final ClientCertRequestImpl request = new ClientCertRequestImpl(callback,
846            keyTypes, principals, host, port);
847        mWebViewClient.onReceivedClientCertRequest(mWebView, request);
848        TraceEvent.end();
849    }
850
851    @Override
852    public void onReceivedLoginRequest(String realm, String account, String args) {
853        TraceEvent.begin();
854        if (TRACE) Log.d(TAG, "onReceivedLoginRequest=" + realm);
855        mWebViewClient.onReceivedLoginRequest(mWebView, realm, account, args);
856        TraceEvent.end();
857    }
858
859    @Override
860    public void onFormResubmission(Message dontResend, Message resend) {
861        TraceEvent.begin();
862        if (TRACE) Log.d(TAG, "onFormResubmission");
863        mWebViewClient.onFormResubmission(mWebView, dontResend, resend);
864        TraceEvent.end();
865    }
866
867    @Override
868    public void onDownloadStart(String url,
869                                String userAgent,
870                                String contentDisposition,
871                                String mimeType,
872                                long contentLength) {
873        if (mDownloadListener != null) {
874            TraceEvent.begin();
875            if (TRACE) Log.d(TAG, "onDownloadStart");
876            mDownloadListener.onDownloadStart(url,
877                                              userAgent,
878                                              contentDisposition,
879                                              mimeType,
880                                              contentLength);
881            TraceEvent.end();
882        }
883    }
884
885    @Override
886    public void showFileChooser(final ValueCallback<String[]> uploadFileCallback,
887            final AwContentsClient.FileChooserParams fileChooserParams) {
888        if (mWebChromeClient == null) {
889            uploadFileCallback.onReceiveValue(null);
890            return;
891        }
892        TraceEvent.begin();
893        FileChooserParamsAdapter adapter = new FileChooserParamsAdapter(
894                fileChooserParams, mWebView.getContext());
895        if (TRACE) Log.d(TAG, "showFileChooser");
896        ValueCallback<Uri[]> callbackAdapter = new ValueCallback<Uri[]>() {
897            private boolean mCompleted;
898            @Override
899            public void onReceiveValue(Uri[] uriList) {
900                if (mCompleted) {
901                    throw new IllegalStateException("showFileChooser result was already called");
902                }
903                mCompleted = true;
904                String s[] = null;
905                if (uriList != null) {
906                    s = new String[uriList.length];
907                    for(int i = 0; i < uriList.length; i++) {
908                        s[i] = uriList[i].toString();
909                    }
910                }
911                uploadFileCallback.onReceiveValue(s);
912            }
913        };
914
915        // Invoke the new callback introduced in Lollipop. If the app handles
916        // it, we're done here.
917        if (mWebChromeClient.onShowFileChooser(mWebView, callbackAdapter, adapter)) {
918            return;
919        }
920
921        // If the app did not handle it and we are running on Lollipop or newer, then
922        // abort.
923        if (mWebView.getContext().getApplicationInfo().targetSdkVersion >=
924                Build.VERSION_CODES.LOLLIPOP) {
925            uploadFileCallback.onReceiveValue(null);
926            return;
927        }
928
929        // Otherwise, for older apps, attempt to invoke the legacy (hidden) API for
930        // backwards compatibility.
931        ValueCallback<Uri> innerCallback = new ValueCallback<Uri>() {
932            private boolean mCompleted;
933            @Override
934            public void onReceiveValue(Uri uri) {
935                if (mCompleted) {
936                    throw new IllegalStateException("showFileChooser result was already called");
937                }
938                mCompleted = true;
939                uploadFileCallback.onReceiveValue(
940                        uri == null ? null : new String[] { uri.toString() });
941            }
942        };
943        if (TRACE) Log.d(TAG, "openFileChooser");
944        mWebChromeClient.openFileChooser(innerCallback, fileChooserParams.acceptTypes,
945                fileChooserParams.capture ? "*" : "");
946        TraceEvent.end();
947    }
948
949    @Override
950    public void onScaleChangedScaled(float oldScale, float newScale) {
951        TraceEvent.begin();
952        if (TRACE) Log.d(TAG, " onScaleChangedScaled");
953        mWebViewClient.onScaleChanged(mWebView, oldScale, newScale);
954        TraceEvent.end();
955    }
956
957    @Override
958    public void onShowCustomView(View view, CustomViewCallback cb) {
959        TraceEvent.begin();
960        if (mWebChromeClient != null) {
961            if (TRACE) Log.d(TAG, "onShowCustomView");
962            mWebChromeClient.onShowCustomView(view, cb);
963        }
964        TraceEvent.end();
965    }
966
967    @Override
968    public void onHideCustomView() {
969        TraceEvent.begin();
970        if (mWebChromeClient != null) {
971            if (TRACE) Log.d(TAG, "onHideCustomView");
972            mWebChromeClient.onHideCustomView();
973        }
974        TraceEvent.end();
975    }
976
977    @Override
978    protected View getVideoLoadingProgressView() {
979        TraceEvent.begin();
980        View result;
981        if (mWebChromeClient != null) {
982            if (TRACE) Log.d(TAG, "getVideoLoadingProgressView");
983            result = mWebChromeClient.getVideoLoadingProgressView();
984        } else {
985            result = null;
986        }
987        TraceEvent.end();
988        return result;
989    }
990
991    @Override
992    public Bitmap getDefaultVideoPoster() {
993        TraceEvent.begin();
994        Bitmap result = null;
995        if (mWebChromeClient != null) {
996            if (TRACE) Log.d(TAG, "getDefaultVideoPoster");
997            result = mWebChromeClient.getDefaultVideoPoster();
998        }
999        if (result == null) {
1000            // The ic_media_video_poster icon is transparent so we need to draw it on a gray
1001            // background.
1002            Bitmap poster = BitmapFactory.decodeResource(
1003                    mWebView.getContext().getResources(),
1004                    R.drawable.ic_media_video_poster);
1005            result = Bitmap.createBitmap(poster.getWidth(), poster.getHeight(), poster.getConfig());
1006            result.eraseColor(Color.GRAY);
1007            Canvas canvas = new Canvas(result);
1008            canvas.drawBitmap(poster, 0f, 0f, null);
1009        }
1010        TraceEvent.end();
1011        return result;
1012    }
1013
1014    // TODO: Move to upstream.
1015    private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler {
1016        private AwHttpAuthHandler mAwHandler;
1017
1018        public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) {
1019            mAwHandler = awHandler;
1020        }
1021
1022        @Override
1023        public void proceed(String username, String password) {
1024            if (username == null) {
1025                username = "";
1026            }
1027
1028            if (password == null) {
1029                password = "";
1030            }
1031            mAwHandler.proceed(username, password);
1032        }
1033
1034        @Override
1035        public void cancel() {
1036            mAwHandler.cancel();
1037        }
1038
1039        @Override
1040        public boolean useHttpAuthUsernamePassword() {
1041            return mAwHandler.isFirstAttempt();
1042        }
1043    }
1044
1045    // TODO: Move to the upstream once the PermissionRequest is part of SDK.
1046    public static class PermissionRequestAdapter extends PermissionRequest {
1047        // TODO: Move the below definitions to AwPermissionRequest.
1048        private static long BITMASK_RESOURCE_VIDEO_CAPTURE = 1 << 1;
1049        private static long BITMASK_RESOURCE_AUDIO_CAPTURE = 1 << 2;
1050        private static long BITMASK_RESOURCE_PROTECTED_MEDIA_ID = 1 << 3;
1051
1052        public static long toAwPermissionResources(String[] resources) {
1053            long result = 0;
1054            for (String resource : resources) {
1055                if (resource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE))
1056                    result |= BITMASK_RESOURCE_VIDEO_CAPTURE;
1057                else if (resource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE))
1058                    result |= BITMASK_RESOURCE_AUDIO_CAPTURE;
1059                else if (resource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID))
1060                    result |= BITMASK_RESOURCE_PROTECTED_MEDIA_ID;
1061            }
1062            return result;
1063        }
1064
1065        private static String[] toPermissionResources(long resources) {
1066            ArrayList<String> result = new ArrayList<String>();
1067            if ((resources & BITMASK_RESOURCE_VIDEO_CAPTURE) != 0)
1068                result.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE);
1069            if ((resources & BITMASK_RESOURCE_AUDIO_CAPTURE) != 0)
1070                result.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE);
1071            if ((resources & BITMASK_RESOURCE_PROTECTED_MEDIA_ID) != 0)
1072                result.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID);
1073            String[] resource_array = new String[result.size()];
1074            return result.toArray(resource_array);
1075        }
1076
1077        private AwPermissionRequest mAwPermissionRequest;
1078        private String[] mResources;
1079
1080        public PermissionRequestAdapter(AwPermissionRequest awPermissionRequest) {
1081            assert awPermissionRequest != null;
1082            mAwPermissionRequest = awPermissionRequest;
1083        }
1084
1085        @Override
1086        public Uri getOrigin() {
1087            return mAwPermissionRequest.getOrigin();
1088        }
1089
1090        @Override
1091        public String[] getResources() {
1092            synchronized (this) {
1093                if (mResources == null) {
1094                    mResources = toPermissionResources(mAwPermissionRequest.getResources());
1095                }
1096                return mResources;
1097            }
1098        }
1099
1100        @Override
1101        public void grant(String[] resources) {
1102            long requestedResource = mAwPermissionRequest.getResources();
1103            if ((requestedResource & toAwPermissionResources(resources)) == requestedResource)
1104                mAwPermissionRequest.grant();
1105            else
1106                mAwPermissionRequest.deny();
1107        }
1108
1109        @Override
1110        public void deny() {
1111            mAwPermissionRequest.deny();
1112        }
1113
1114    }
1115}
1116