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