/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.webview.chromium; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Picture; import android.net.http.ErrorStrings; import android.net.http.SslError; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.provider.Browser; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.webkit.ClientCertRequest; import android.webkit.ConsoleMessage; import android.webkit.DownloadListener; import android.webkit.GeolocationPermissions; import android.webkit.JsDialogHelper; import android.webkit.JsPromptResult; import android.webkit.JsResult; import android.webkit.PermissionRequest; import android.webkit.SslErrorHandler; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebChromeClient.CustomViewCallback; import android.webkit.WebResourceResponse; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; import org.chromium.android_webview.AwContentsClient; import org.chromium.android_webview.AwContentsClientBridge; import org.chromium.android_webview.AwHttpAuthHandler; import org.chromium.android_webview.AwWebResourceResponse; import org.chromium.android_webview.JsPromptResultReceiver; import org.chromium.android_webview.JsResultReceiver; import org.chromium.android_webview.permission.AwPermissionRequest; import org.chromium.base.ThreadUtils; import org.chromium.base.TraceEvent; import org.chromium.content.browser.ContentView; import org.chromium.content.browser.ContentViewClient; import java.lang.ref.WeakReference; import java.net.URISyntaxException; import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; /** * An adapter class that forwards the callbacks from {@link ContentViewClient} * to the appropriate {@link WebViewClient} or {@link WebChromeClient}. * * An instance of this class is associated with one {@link WebViewChromium} * instance. A WebViewChromium is a WebView implementation provider (that is * android.webkit.WebView delegates all functionality to it) and has exactly * one corresponding {@link ContentView} instance. * * A {@link ContentViewClient} may be shared between multiple {@link ContentView}s, * and hence multiple WebViews. Many WebViewClient methods pass the source * WebView as an argument. This means that we either need to pass the * corresponding ContentView to the corresponding ContentViewClient methods, * or use an instance of ContentViewClientAdapter per WebViewChromium, to * allow the source WebView to be injected by ContentViewClientAdapter. We * choose the latter, because it makes for a cleaner design. */ public class WebViewContentsClientAdapter extends AwContentsClient { // TAG is chosen for consistency with classic webview tracing. private static final String TAG = "WebViewCallback"; // Enables API callback tracing private static final boolean TRACE = android.webkit.DebugFlags.TRACE_CALLBACK; // The WebView instance that this adapter is serving. private final WebView mWebView; // The WebViewClient instance that was passed to WebView.setWebViewClient(). private WebViewClient mWebViewClient; // The WebChromeClient instance that was passed to WebView.setContentViewClient(). private WebChromeClient mWebChromeClient; // The listener receiving find-in-page API results. private WebView.FindListener mFindListener; // The listener receiving notifications of screen updates. private WebView.PictureListener mPictureListener; private DownloadListener mDownloadListener; private Handler mUiThreadHandler; private static final int NEW_WEBVIEW_CREATED = 100; private WeakHashMap> mOngoingPermissionRequests; /** * Adapter constructor. * * @param webView the {@link WebView} instance that this adapter is serving. */ WebViewContentsClientAdapter(WebView webView) { if (webView == null) { throw new IllegalArgumentException("webView can't be null"); } mWebView = webView; setWebViewClient(null); mUiThreadHandler = new Handler() { @Override public void handleMessage(Message msg) { switch(msg.what) { case NEW_WEBVIEW_CREATED: WebView.WebViewTransport t = (WebView.WebViewTransport) msg.obj; WebView newWebView = t.getWebView(); if (newWebView == mWebView) { throw new IllegalArgumentException( "Parent WebView cannot host it's own popup window. Please " + "use WebSettings.setSupportMultipleWindows(false)"); } if (newWebView != null && newWebView.copyBackForwardList().getSize() != 0) { throw new IllegalArgumentException( "New WebView for popup window must not have been previously " + "navigated."); } WebViewChromium.completeWindowCreation(mWebView, newWebView); break; default: throw new IllegalStateException(); } } }; } // WebViewClassic is coded in such a way that even if a null WebViewClient is set, // certain actions take place. // We choose to replicate this behavior by using a NullWebViewClient implementation (also known // as the Null Object pattern) rather than duplicating the WebViewClassic approach in // ContentView. static class NullWebViewClient extends WebViewClient { @Override public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { // TODO: Investigate more and add a test case. // This is reflecting Clank's behavior. int keyCode = event.getKeyCode(); return !ContentViewClient.shouldPropagateKey(keyCode); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Intent intent; // Perform generic parsing of the URI to turn it into an Intent. try { intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); } catch (URISyntaxException ex) { Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage()); return false; } // Sanitize the Intent, ensuring web pages can not bypass browser // security (only access to BROWSABLE activities). intent.addCategory(Intent.CATEGORY_BROWSABLE); intent.setComponent(null); Intent selector = intent.getSelector(); if (selector != null) { selector.addCategory(Intent.CATEGORY_BROWSABLE); selector.setComponent(null); } // Pass the package name as application ID so that the intent from the // same application can be opened in the same tab. intent.putExtra(Browser.EXTRA_APPLICATION_ID, view.getContext().getPackageName()); try { view.getContext().startActivity(intent); } catch (ActivityNotFoundException ex) { Log.w(TAG, "No application can handle " + url); return false; } return true; } } void setWebViewClient(WebViewClient client) { if (client != null) { mWebViewClient = client; } else { mWebViewClient = new NullWebViewClient(); } } void setWebChromeClient(WebChromeClient client) { mWebChromeClient = client; } void setDownloadListener(DownloadListener listener) { mDownloadListener = listener; } void setFindListener(WebView.FindListener listener) { mFindListener = listener; } void setPictureListener(WebView.PictureListener listener) { mPictureListener = listener; } //-------------------------------------------------------------------------------------------- // Adapter for all the methods. //-------------------------------------------------------------------------------------------- /** * @see AwContentsClient#getVisitedHistory */ @Override public void getVisitedHistory(ValueCallback callback) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "getVisitedHistory"); mWebChromeClient.getVisitedHistory(callback); } TraceEvent.end(); } /** * @see AwContentsClient#doUpdateVisiteHistory(String, boolean) */ @Override public void doUpdateVisitedHistory(String url, boolean isReload) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "doUpdateVisitedHistory=" + url + " reload=" + isReload); mWebViewClient.doUpdateVisitedHistory(mWebView, url, isReload); TraceEvent.end(); } /** * @see AwContentsClient#onProgressChanged(int) */ @Override public void onProgressChanged(int progress) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onProgressChanged=" + progress); mWebChromeClient.onProgressChanged(mWebView, progress); } TraceEvent.end(); } private static class WebResourceRequestImpl implements WebResourceRequest { private final ShouldInterceptRequestParams mParams; public WebResourceRequestImpl(ShouldInterceptRequestParams params) { mParams = params; } @Override public Uri getUrl() { return Uri.parse(mParams.url); } @Override public boolean isForMainFrame() { return mParams.isMainFrame; } @Override public boolean hasGesture() { return mParams.hasUserGesture; } @Override public String getMethod() { return mParams.method; } @Override public Map getRequestHeaders() { return mParams.requestHeaders; } } /** * @see AwContentsClient#shouldInterceptRequest(java.lang.String) */ @Override public AwWebResourceResponse shouldInterceptRequest(ShouldInterceptRequestParams params) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "shouldInterceptRequest=" + params.url); WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, new WebResourceRequestImpl(params)); TraceEvent.end(); if (response == null) return null; // AwWebResourceResponse should support null headers. b/16332774. Map responseHeaders = response.getResponseHeaders(); if (responseHeaders == null) responseHeaders = new HashMap(); return new AwWebResourceResponse( response.getMimeType(), response.getEncoding(), response.getData(), response.getStatusCode(), response.getReasonPhrase(), responseHeaders); } /** * @see AwContentsClient#shouldOverrideUrlLoading(java.lang.String) */ @Override public boolean shouldOverrideUrlLoading(String url) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "shouldOverrideUrlLoading=" + url); boolean result = mWebViewClient.shouldOverrideUrlLoading(mWebView, url); TraceEvent.end(); return result; } /** * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent) */ @Override public void onUnhandledKeyEvent(KeyEvent event) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onUnhandledKeyEvent"); mWebViewClient.onUnhandledKeyEvent(mWebView, event); TraceEvent.end(); } /** * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage) */ @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { TraceEvent.begin(); boolean result; if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onConsoleMessage: " + consoleMessage.message()); result = mWebChromeClient.onConsoleMessage(consoleMessage); String message = consoleMessage.message(); if (result && message != null && message.startsWith("[blocked]")) { Log.e(TAG, "Blocked URL: " + message); } } else { result = false; } TraceEvent.end(); return result; } /** * @see AwContentsClient#onFindResultReceived(int,int,boolean) */ @Override public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { if (mFindListener == null) return; TraceEvent.begin(); if (TRACE) Log.d(TAG, "onFindResultReceived"); mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); TraceEvent.end(); } /** * @See AwContentsClient#onNewPicture(Picture) */ @Override public void onNewPicture(Picture picture) { if (mPictureListener == null) return; TraceEvent.begin(); if (TRACE) Log.d(TAG, "onNewPicture"); mPictureListener.onNewPicture(mWebView, picture); TraceEvent.end(); } @Override public void onLoadResource(String url) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onLoadResource=" + url); mWebViewClient.onLoadResource(mWebView, url); TraceEvent.end(); } @Override public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) { Message m = mUiThreadHandler.obtainMessage( NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport()); TraceEvent.begin(); boolean result; if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onCreateWindow"); result = mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m); } else { result = false; } TraceEvent.end(); return result; } /** * @see AwContentsClient#onCloseWindow() */ @Override public void onCloseWindow() { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onCloseWindow"); mWebChromeClient.onCloseWindow(mWebView); } TraceEvent.end(); } /** * @see AwContentsClient#onRequestFocus() */ @Override public void onRequestFocus() { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onRequestFocus"); mWebChromeClient.onRequestFocus(mWebView); } TraceEvent.end(); } /** * @see AwContentsClient#onReceivedTouchIconUrl(String url, boolean precomposed) */ @Override public void onReceivedTouchIconUrl(String url, boolean precomposed) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onReceivedTouchIconUrl=" + url); mWebChromeClient.onReceivedTouchIconUrl(mWebView, url, precomposed); } TraceEvent.end(); } /** * @see AwContentsClient#onReceivedIcon(Bitmap bitmap) */ @Override public void onReceivedIcon(Bitmap bitmap) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onReceivedIcon"); mWebChromeClient.onReceivedIcon(mWebView, bitmap); } TraceEvent.end(); } /** * @see ContentViewClient#onPageStarted(String) */ @Override public void onPageStarted(String url) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onPageStarted=" + url); mWebViewClient.onPageStarted(mWebView, url, mWebView.getFavicon()); TraceEvent.end(); } /** * @see ContentViewClient#onPageFinished(String) */ @Override public void onPageFinished(String url) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onPageFinished=" + url); mWebViewClient.onPageFinished(mWebView, url); TraceEvent.end(); // See b/8208948 // This fakes an onNewPicture callback after onPageFinished to allow // CTS tests to run in an un-flaky manner. This is required as the // path for sending Picture updates in Chromium are decoupled from the // page loading callbacks, i.e. the Chrome compositor may draw our // content and send the Picture before onPageStarted or onPageFinished // are invoked. The CTS harness discards any pictures it receives before // onPageStarted is invoked, so in the case we get the Picture before that and // no further updates after onPageStarted, we'll fail the test by timing // out waiting for a Picture. // To ensure backwards compatibility, we need to defer sending Picture updates // until onPageFinished has been invoked. This work is being done // upstream, and we can revert this hack when it lands. if (mPictureListener != null) { ThreadUtils.postOnUiThreadDelayed(new Runnable() { @Override public void run() { UnimplementedWebViewApi.invoke(); if (mPictureListener != null) { if (TRACE) Log.d(TAG, "onPageFinished-fake"); mPictureListener.onNewPicture(mWebView, new Picture()); } } }, 100); } } /** * @see ContentViewClient#onReceivedError(int,String,String) */ @Override public void onReceivedError(int errorCode, String description, String failingUrl) { if (description == null || description.isEmpty()) { // ErrorStrings is @hidden, so we can't do this in AwContents. // Normally the net/ layer will set a valid description, but for synthesized callbacks // (like in the case for intercepted requests) AwContents will pass in null. description = ErrorStrings.getString(errorCode, mWebView.getContext()); } TraceEvent.begin(); if (TRACE) Log.d(TAG, "onReceivedError=" + failingUrl); mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl); TraceEvent.end(); } /** * @see ContentViewClient#onReceivedTitle(String) */ @Override public void onReceivedTitle(String title) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onReceivedTitle"); mWebChromeClient.onReceivedTitle(mWebView, title); } TraceEvent.end(); } /** * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent) */ @Override public boolean shouldOverrideKeyEvent(KeyEvent event) { // The check below is reflecting Clank's behavior and is a workaround for http://b/7697782. // 1. The check for system key should be made in AwContents or ContentViewCore, before // shouldOverrideKeyEvent() is called at all. // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from // dispatchKeyEvent(). if (!ContentViewClient.shouldPropagateKey(event.getKeyCode())) return true; TraceEvent.begin(); if (TRACE) Log.d(TAG, "shouldOverrideKeyEvent"); boolean result = mWebViewClient.shouldOverrideKeyEvent(mWebView, event); TraceEvent.end(); return result; } /** * @see ContentViewClient#onStartContentIntent(Context, String) * Callback when detecting a click on a content link. */ // TODO: Delete this method when removed from base class. public void onStartContentIntent(Context context, String contentUrl) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "shouldOverrideUrlLoading=" + contentUrl); mWebViewClient.shouldOverrideUrlLoading(mWebView, contentUrl); TraceEvent.end(); } @Override public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onGeolocationPermissionsShowPrompt"); mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); } TraceEvent.end(); } @Override public void onGeolocationPermissionsHidePrompt() { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onGeolocationPermissionsHidePrompt"); mWebChromeClient.onGeolocationPermissionsHidePrompt(); } TraceEvent.end(); } @Override public void onPermissionRequest(AwPermissionRequest permissionRequest) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onPermissionRequest"); if (mOngoingPermissionRequests == null) { mOngoingPermissionRequests = new WeakHashMap>(); } PermissionRequestAdapter adapter = new PermissionRequestAdapter(permissionRequest); mOngoingPermissionRequests.put( permissionRequest, new WeakReference(adapter)); mWebChromeClient.onPermissionRequest(adapter); } else { // By default, we deny the permission. permissionRequest.deny(); } TraceEvent.end(); } @Override public void onPermissionRequestCanceled(AwPermissionRequest permissionRequest) { TraceEvent.begin(); if (mWebChromeClient != null && mOngoingPermissionRequests != null) { if (TRACE) Log.d(TAG, "onPermissionRequestCanceled"); WeakReference weakRef = mOngoingPermissionRequests.get(permissionRequest); // We don't hold strong reference to PermissionRequestAdpater and don't expect the // user only holds weak reference to it either, if so, user has no way to call // grant()/deny(), and no need to be notified the cancellation of request. if (weakRef != null) { PermissionRequestAdapter adapter = weakRef.get(); if (adapter != null) mWebChromeClient.onPermissionRequestCanceled(adapter); } } TraceEvent.end(); } private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver { private JsPromptResultReceiver mChromePromptResultReceiver; private JsResultReceiver mChromeResultReceiver; // We hold onto the JsPromptResult here, just to avoid the need to downcast // in onJsResultComplete. private final JsPromptResult mPromptResult = new JsPromptResult(this); public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) { mChromePromptResultReceiver = receiver; } public JsPromptResultReceiverAdapter(JsResultReceiver receiver) { mChromeResultReceiver = receiver; } public JsPromptResult getPromptResult() { return mPromptResult; } @Override public void onJsResultComplete(JsResult result) { if (mChromePromptResultReceiver != null) { if (mPromptResult.getResult()) { mChromePromptResultReceiver.confirm(mPromptResult.getStringResult()); } else { mChromePromptResultReceiver.cancel(); } } else { if (mPromptResult.getResult()) { mChromeResultReceiver.confirm(); } else { mChromeResultReceiver.cancel(); } } } } @Override public void handleJsAlert(String url, String message, JsResultReceiver receiver) { TraceEvent.begin(); if (mWebChromeClient != null) { final JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); if (TRACE) Log.d(TAG, "onJsAlert"); if (!mWebChromeClient.onJsAlert(mWebView, url, message, res)) { new JsDialogHelper(res, JsDialogHelper.ALERT, null, message, url) .showDialog(mWebView.getContext()); } } else { receiver.cancel(); } TraceEvent.end(); } @Override public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) { TraceEvent.begin(); if (mWebChromeClient != null) { final JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); if (TRACE) Log.d(TAG, "onJsBeforeUnload"); if (!mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res)) { new JsDialogHelper(res, JsDialogHelper.UNLOAD, null, message, url) .showDialog(mWebView.getContext()); } } else { receiver.cancel(); } TraceEvent.end(); } @Override public void handleJsConfirm(String url, String message, JsResultReceiver receiver) { TraceEvent.begin(); if (mWebChromeClient != null) { final JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); if (TRACE) Log.d(TAG, "onJsConfirm"); if (!mWebChromeClient.onJsConfirm(mWebView, url, message, res)) { new JsDialogHelper(res, JsDialogHelper.CONFIRM, null, message, url) .showDialog(mWebView.getContext()); } } else { receiver.cancel(); } TraceEvent.end(); } @Override public void handleJsPrompt(String url, String message, String defaultValue, JsPromptResultReceiver receiver) { TraceEvent.begin(); if (mWebChromeClient != null) { final JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); if (TRACE) Log.d(TAG, "onJsPrompt"); if (!mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res)) { new JsDialogHelper(res, JsDialogHelper.PROMPT, defaultValue, message, url) .showDialog(mWebView.getContext()); } } else { receiver.cancel(); } TraceEvent.end(); } @Override public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onReceivedHttpAuthRequest=" + host); mWebViewClient.onReceivedHttpAuthRequest(mWebView, new AwHttpAuthHandlerAdapter(handler), host, realm); TraceEvent.end(); } @Override public void onReceivedSslError(final ValueCallback callback, SslError error) { SslErrorHandler handler = new SslErrorHandler() { @Override public void proceed() { callback.onReceiveValue(true); } @Override public void cancel() { callback.onReceiveValue(false); } }; TraceEvent.begin(); if (TRACE) Log.d(TAG, "onReceivedSslError"); mWebViewClient.onReceivedSslError(mWebView, handler, error); TraceEvent.end(); } private static class ClientCertRequestImpl extends ClientCertRequest { final private AwContentsClientBridge.ClientCertificateRequestCallback mCallback; final private String[] mKeyTypes; final private Principal[] mPrincipals; final private String mHost; final private int mPort; public ClientCertRequestImpl( AwContentsClientBridge.ClientCertificateRequestCallback callback, String[] keyTypes, Principal[] principals, String host, int port) { mCallback = callback; mKeyTypes = keyTypes; mPrincipals = principals; mHost = host; mPort = port; } @Override public String[] getKeyTypes() { // This is already a copy of native argument, so return directly. return mKeyTypes; } @Override public Principal[] getPrincipals() { // This is already a copy of native argument, so return directly. return mPrincipals; } @Override public String getHost() { return mHost; } @Override public int getPort() { return mPort; } @Override public void proceed(final PrivateKey privateKey, final X509Certificate[] chain) { mCallback.proceed(privateKey, chain); } @Override public void ignore() { mCallback.ignore(); } @Override public void cancel() { mCallback.cancel(); } } @Override public void onReceivedClientCertRequest( AwContentsClientBridge.ClientCertificateRequestCallback callback, String[] keyTypes, Principal[] principals, String host, int port) { if (TRACE) Log.d(TAG, "onReceivedClientCertRequest"); TraceEvent.begin(); final ClientCertRequestImpl request = new ClientCertRequestImpl(callback, keyTypes, principals, host, port); mWebViewClient.onReceivedClientCertRequest(mWebView, request); TraceEvent.end(); } @Override public void onReceivedLoginRequest(String realm, String account, String args) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onReceivedLoginRequest=" + realm); mWebViewClient.onReceivedLoginRequest(mWebView, realm, account, args); TraceEvent.end(); } @Override public void onFormResubmission(Message dontResend, Message resend) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onFormResubmission"); mWebViewClient.onFormResubmission(mWebView, dontResend, resend); TraceEvent.end(); } @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) { if (mDownloadListener != null) { TraceEvent.begin(); if (TRACE) Log.d(TAG, "onDownloadStart"); mDownloadListener.onDownloadStart(url, userAgent, contentDisposition, mimeType, contentLength); TraceEvent.end(); } } @Override public void showFileChooser(final ValueCallback uploadFileCallback, final AwContentsClient.FileChooserParams fileChooserParams) { if (mWebChromeClient == null) { uploadFileCallback.onReceiveValue(null); return; } TraceEvent.begin(); FileChooserParamsAdapter adapter = new FileChooserParamsAdapter( fileChooserParams, mWebView.getContext()); if (TRACE) Log.d(TAG, "showFileChooser"); ValueCallback callbackAdapter = new ValueCallback() { private boolean mCompleted; @Override public void onReceiveValue(Uri[] uriList) { if (mCompleted) { throw new IllegalStateException("showFileChooser result was already called"); } mCompleted = true; String s[] = null; if (uriList != null) { s = new String[uriList.length]; for(int i = 0; i < uriList.length; i++) { s[i] = uriList[i].toString(); } } uploadFileCallback.onReceiveValue(s); } }; if (mWebChromeClient.onShowFileChooser(mWebView, callbackAdapter, adapter)) { return; } if (mWebView.getContext().getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.KITKAT) { uploadFileCallback.onReceiveValue(null); return; } ValueCallback innerCallback = new ValueCallback() { private boolean mCompleted; @Override public void onReceiveValue(Uri uri) { if (mCompleted) { throw new IllegalStateException("showFileChooser result was already called"); } mCompleted = true; uploadFileCallback.onReceiveValue( uri == null ? null : new String[] { uri.toString() }); } }; if (TRACE) Log.d(TAG, "openFileChooser"); mWebChromeClient.openFileChooser(innerCallback, fileChooserParams.acceptTypes, fileChooserParams.capture ? "*" : ""); TraceEvent.end(); } @Override public void onScaleChangedScaled(float oldScale, float newScale) { TraceEvent.begin(); if (TRACE) Log.d(TAG, " onScaleChangedScaled"); mWebViewClient.onScaleChanged(mWebView, oldScale, newScale); TraceEvent.end(); } @Override public void onShowCustomView(View view, CustomViewCallback cb) { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onShowCustomView"); mWebChromeClient.onShowCustomView(view, cb); } TraceEvent.end(); } @Override public void onHideCustomView() { TraceEvent.begin(); if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "onHideCustomView"); mWebChromeClient.onHideCustomView(); } TraceEvent.end(); } @Override protected View getVideoLoadingProgressView() { TraceEvent.begin(); View result; if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "getVideoLoadingProgressView"); result = mWebChromeClient.getVideoLoadingProgressView(); } else { result = null; } TraceEvent.end(); return result; } @Override public Bitmap getDefaultVideoPoster() { TraceEvent.begin(); Bitmap result = null; if (mWebChromeClient != null) { if (TRACE) Log.d(TAG, "getDefaultVideoPoster"); result = mWebChromeClient.getDefaultVideoPoster(); } if (result == null) { // The ic_media_video_poster icon is transparent so we need to draw it on a gray // background. Bitmap poster = BitmapFactory.decodeResource( mWebView.getContext().getResources(), R.drawable.ic_media_video_poster); result = Bitmap.createBitmap(poster.getWidth(), poster.getHeight(), poster.getConfig()); result.eraseColor(Color.GRAY); Canvas canvas = new Canvas(result); canvas.drawBitmap(poster, 0f, 0f, null); } TraceEvent.end(); return result; } // TODO: Move to upstream. private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler { private AwHttpAuthHandler mAwHandler; public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) { mAwHandler = awHandler; } @Override public void proceed(String username, String password) { if (username == null) { username = ""; } if (password == null) { password = ""; } mAwHandler.proceed(username, password); } @Override public void cancel() { mAwHandler.cancel(); } @Override public boolean useHttpAuthUsernamePassword() { return mAwHandler.isFirstAttempt(); } } // TODO: Move to the upstream once the PermissionRequest is part of SDK. public static class PermissionRequestAdapter extends PermissionRequest { // TODO: Move the below definitions to AwPermissionRequest. private static long BITMASK_RESOURCE_VIDEO_CAPTURE = 1 << 1; private static long BITMASK_RESOURCE_AUDIO_CAPTURE = 1 << 2; private static long BITMASK_RESOURCE_PROTECTED_MEDIA_ID = 1 << 3; public static long toAwPermissionResources(String[] resources) { long result = 0; for (String resource : resources) { if (resource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) result |= BITMASK_RESOURCE_VIDEO_CAPTURE; else if (resource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) result |= BITMASK_RESOURCE_AUDIO_CAPTURE; else if (resource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) result |= BITMASK_RESOURCE_PROTECTED_MEDIA_ID; } return result; } private static String[] toPermissionResources(long resources) { ArrayList result = new ArrayList(); if ((resources & BITMASK_RESOURCE_VIDEO_CAPTURE) != 0) result.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE); if ((resources & BITMASK_RESOURCE_AUDIO_CAPTURE) != 0) result.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE); if ((resources & BITMASK_RESOURCE_PROTECTED_MEDIA_ID) != 0) result.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID); String[] resource_array = new String[result.size()]; return result.toArray(resource_array); } private AwPermissionRequest mAwPermissionRequest; private String[] mResources; public PermissionRequestAdapter(AwPermissionRequest awPermissionRequest) { assert awPermissionRequest != null; mAwPermissionRequest = awPermissionRequest; } @Override public Uri getOrigin() { return mAwPermissionRequest.getOrigin(); } @Override public String[] getResources() { synchronized (this) { if (mResources == null) { mResources = toPermissionResources(mAwPermissionRequest.getResources()); } return mResources; } } @Override public void grant(String[] resources) { long requestedResource = mAwPermissionRequest.getResources(); if ((requestedResource & toAwPermissionResources(resources)) == requestedResource) mAwPermissionRequest.grant(); else mAwPermissionRequest.deny(); } @Override public void deny() { mAwPermissionRequest.deny(); } } }