Minor bug fixes for Webview, Permission request support (#1723)
* fix: Match URLs with trailing / * Handle permission requests and attempt to enable Widevine * Tie CEF loglevel to server debug logs * Lint * Add missing file Forgot to add in previous commits * Provide WebResourceResponse * Fix NullException if headers are not set * fix: Don't allow interception for initial page load fixes #1713 * Lint
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 android.webkit;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* This class defines a permission request and is used when web content
|
||||
* requests access to protected resources. The permission request related events
|
||||
* are delivered via {@link WebChromeClient#onPermissionRequest} and
|
||||
* {@link WebChromeClient#onPermissionRequestCanceled}.
|
||||
*
|
||||
* Either {@link #grant(String[]) grant()} or {@link #deny()} must be called in UI
|
||||
* thread to respond to the request.
|
||||
*
|
||||
* New protected resources whose names are not defined here may be requested in
|
||||
* future versions of WebView, even when running on an older Android release. To
|
||||
* avoid unintentionally granting requests for new permissions, you should pass the
|
||||
* specific permissions you intend to grant to {@link #grant(String[]) grant()},
|
||||
* and avoid writing code like this example:
|
||||
* <pre class="prettyprint">
|
||||
* permissionRequest.grant(permissionRequest.getResources()) // This is wrong!!!
|
||||
* </pre>
|
||||
* See the WebView's release notes for information about new protected resources.
|
||||
*/
|
||||
public abstract class PermissionRequest {
|
||||
/**
|
||||
* Resource belongs to video capture device, like camera.
|
||||
*/
|
||||
public final static String RESOURCE_VIDEO_CAPTURE = "android.webkit.resource.VIDEO_CAPTURE";
|
||||
/**
|
||||
* Resource belongs to audio capture device, like microphone.
|
||||
*/
|
||||
public final static String RESOURCE_AUDIO_CAPTURE = "android.webkit.resource.AUDIO_CAPTURE";
|
||||
/**
|
||||
* Resource belongs to protected media identifier.
|
||||
* After the user grants this resource, the origin can use EME APIs to generate the license
|
||||
* requests.
|
||||
*/
|
||||
public final static String RESOURCE_PROTECTED_MEDIA_ID =
|
||||
"android.webkit.resource.PROTECTED_MEDIA_ID";
|
||||
/**
|
||||
* Resource will allow sysex messages to be sent to or received from MIDI devices. These
|
||||
* messages are privileged operations, e.g. modifying sound libraries and sampling data, or
|
||||
* even updating the MIDI device's firmware.
|
||||
*
|
||||
* Permission may be requested for this resource in API levels 21 and above, if the Android
|
||||
* device has been updated to WebView 45 or above.
|
||||
*/
|
||||
public final static String RESOURCE_MIDI_SYSEX = "android.webkit.resource.MIDI_SYSEX";
|
||||
|
||||
/**
|
||||
* Call this method to get the origin of the web page which is trying to access
|
||||
* the restricted resources.
|
||||
*
|
||||
* @return the origin of web content which attempt to access the restricted
|
||||
* resources.
|
||||
*/
|
||||
public abstract Uri getOrigin();
|
||||
|
||||
/**
|
||||
* Call this method to get the resources the web page is trying to access.
|
||||
*
|
||||
* @return the array of resources the web content wants to access.
|
||||
*/
|
||||
public abstract String[] getResources();
|
||||
|
||||
/**
|
||||
* Call this method to grant origin the permission to access the given resources.
|
||||
* The granted permission is only valid for this WebView.
|
||||
*
|
||||
* @param resources the resources granted to be accessed by origin, to grant
|
||||
* request, the requested resources returned by {@link #getResources()}
|
||||
* must be equals or a subset of granted resources.
|
||||
* This parameter is designed to avoid granting permission by accident
|
||||
* especially when new resources are requested by web content.
|
||||
*/
|
||||
public abstract void grant(String[] resources);
|
||||
|
||||
/**
|
||||
* Call this method to deny the request.
|
||||
*/
|
||||
public abstract void deny();
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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 android.webkit;
|
||||
|
||||
import android.annotation.SystemApi;
|
||||
import android.os.Build;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringBufferInputStream;
|
||||
import java.util.Map;
|
||||
import android.annotation.NonNull;
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulates a resource response. Applications can return an instance of this
|
||||
* class from {@link WebViewClient#shouldInterceptRequest} to provide a custom
|
||||
* response when the WebView requests a particular resource.
|
||||
*/
|
||||
public class WebResourceResponse {
|
||||
private boolean mImmutable;
|
||||
private String mMimeType;
|
||||
private String mEncoding;
|
||||
private int mStatusCode;
|
||||
private String mReasonPhrase;
|
||||
private Map<String, String> mResponseHeaders;
|
||||
private InputStream mInputStream;
|
||||
|
||||
/**
|
||||
* Constructs a resource response with the given MIME type, character encoding,
|
||||
* and input stream. Callers must implement {@link InputStream#read(byte[])} for
|
||||
* the input stream. {@link InputStream#close()} will be called after the WebView
|
||||
* has finished with the response.
|
||||
*
|
||||
* <p class="note"><b>Note:</b> The MIME type and character encoding must
|
||||
* be specified as separate parameters (for example {@code "text/html"} and
|
||||
* {@code "utf-8"}), not a single value like the {@code "text/html; charset=utf-8"}
|
||||
* format used in the HTTP Content-Type header. Do not use the value of a HTTP
|
||||
* Content-Encoding header for {@code encoding}, as that header does not specify a
|
||||
* character encoding. Content without a defined character encoding (for example
|
||||
* image resources) should pass {@code null} for {@code encoding}.
|
||||
*
|
||||
* @param mimeType the resource response's MIME type, for example {@code "text/html"}.
|
||||
* @param encoding the resource response's character encoding, for example {@code "utf-8"}.
|
||||
* @param data the input stream that provides the resource response's data. Must not be a
|
||||
* StringBufferInputStream.
|
||||
*/
|
||||
public WebResourceResponse(String mimeType, String encoding,
|
||||
InputStream data) {
|
||||
mMimeType = mimeType;
|
||||
mEncoding = encoding;
|
||||
setData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a resource response with the given parameters. Callers must implement
|
||||
* {@link InputStream#read(byte[])} for the input stream. {@link InputStream#close()} will be
|
||||
* called after the WebView has finished with the response.
|
||||
*
|
||||
*
|
||||
* <p class="note"><b>Note:</b> See {@link #WebResourceResponse(String,String,InputStream)}
|
||||
* for details on what should be specified for {@code mimeType} and {@code encoding}.
|
||||
*
|
||||
* @param mimeType the resource response's MIME type, for example {@code "text/html"}.
|
||||
* @param encoding the resource response's character encoding, for example {@code "utf-8"}.
|
||||
* @param statusCode the status code needs to be in the ranges [100, 299], [400, 599].
|
||||
* Causing a redirect by specifying a 3xx code is not supported.
|
||||
* @param reasonPhrase the phrase describing the status code, for example "OK". Must be
|
||||
* non-empty.
|
||||
* @param responseHeaders the resource response's headers represented as a mapping of header
|
||||
* name -> header value.
|
||||
* @param data the input stream that provides the resource response's data. Must not be a
|
||||
* StringBufferInputStream.
|
||||
*/
|
||||
public WebResourceResponse(String mimeType, String encoding, int statusCode,
|
||||
@NonNull String reasonPhrase, Map<String, String> responseHeaders, InputStream data) {
|
||||
this(mimeType, encoding, data);
|
||||
setStatusCodeAndReasonPhrase(statusCode, reasonPhrase);
|
||||
setResponseHeaders(responseHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource response's MIME type, for example "text/html".
|
||||
*
|
||||
* @param mimeType The resource response's MIME type
|
||||
*/
|
||||
public void setMimeType(String mimeType) {
|
||||
checkImmutable();
|
||||
mMimeType = mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resource response's MIME type.
|
||||
*
|
||||
* @return The resource response's MIME type
|
||||
*/
|
||||
public String getMimeType() {
|
||||
return mMimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource response's encoding, for example "UTF-8". This is used
|
||||
* to decode the data from the input stream.
|
||||
*
|
||||
* @param encoding The resource response's encoding
|
||||
*/
|
||||
public void setEncoding(String encoding) {
|
||||
checkImmutable();
|
||||
mEncoding = encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resource response's encoding.
|
||||
*
|
||||
* @return The resource response's encoding
|
||||
*/
|
||||
public String getEncoding() {
|
||||
return mEncoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource response's status code and reason phrase.
|
||||
*
|
||||
* @param statusCode the status code needs to be in the ranges [100, 299], [400, 599].
|
||||
* Causing a redirect by specifying a 3xx code is not supported.
|
||||
* @param reasonPhrase the phrase describing the status code, for example "OK". Must be
|
||||
* non-empty.
|
||||
*/
|
||||
public void setStatusCodeAndReasonPhrase(int statusCode, @NonNull String reasonPhrase) {
|
||||
checkImmutable();
|
||||
if (statusCode < 100)
|
||||
throw new IllegalArgumentException("statusCode can't be less than 100.");
|
||||
if (statusCode > 599)
|
||||
throw new IllegalArgumentException("statusCode can't be greater than 599.");
|
||||
if (statusCode > 299 && statusCode < 400)
|
||||
throw new IllegalArgumentException("statusCode can't be in the [300, 399] range.");
|
||||
if (reasonPhrase == null)
|
||||
throw new IllegalArgumentException("reasonPhrase can't be null.");
|
||||
if (reasonPhrase.trim().isEmpty())
|
||||
throw new IllegalArgumentException("reasonPhrase can't be empty.");
|
||||
for (int i = 0; i < reasonPhrase.length(); i++) {
|
||||
int c = reasonPhrase.charAt(i);
|
||||
if (c > 0x7F) {
|
||||
throw new IllegalArgumentException(
|
||||
"reasonPhrase can't contain non-ASCII characters.");
|
||||
}
|
||||
}
|
||||
mStatusCode = statusCode;
|
||||
mReasonPhrase = reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resource response's status code.
|
||||
*
|
||||
* @return The resource response's status code.
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return mStatusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the description of the resource response's status code.
|
||||
*
|
||||
* @return The description of the resource response's status code.
|
||||
*/
|
||||
public String getReasonPhrase() {
|
||||
return mReasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the headers for the resource response.
|
||||
*
|
||||
* @param headers Mapping of header name -> header value.
|
||||
*/
|
||||
public void setResponseHeaders(Map<String, String> headers) {
|
||||
checkImmutable();
|
||||
mResponseHeaders = headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the headers for the resource response.
|
||||
*
|
||||
* @return The headers for the resource response.
|
||||
*/
|
||||
public Map<String, String> getResponseHeaders() {
|
||||
return mResponseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input stream that provides the resource response's data. Callers
|
||||
* must implement {@link InputStream#read(byte[])}. {@link InputStream#close()}
|
||||
* will be called after the WebView has finished with the response.
|
||||
*
|
||||
* @param data the input stream that provides the resource response's data. Must not be a
|
||||
* StringBufferInputStream.
|
||||
*/
|
||||
public void setData(InputStream data) {
|
||||
checkImmutable();
|
||||
// If data is (or is a subclass of) StringBufferInputStream
|
||||
if (data != null && StringBufferInputStream.class.isAssignableFrom(data.getClass())) {
|
||||
throw new IllegalArgumentException("StringBufferInputStream is deprecated and must " +
|
||||
"not be passed to a WebResourceResponse");
|
||||
}
|
||||
mInputStream = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input stream that provides the resource response's data.
|
||||
*
|
||||
* @return The input stream that provides the resource response's data
|
||||
*/
|
||||
public InputStream getData() {
|
||||
return mInputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal version of the constructor that doesn't perform arguments checks.
|
||||
* @hide
|
||||
*/
|
||||
@SystemApi
|
||||
public WebResourceResponse(boolean immutable, String mimeType, String encoding, int statusCode,
|
||||
String reasonPhrase, Map<String, String> responseHeaders, InputStream data) {
|
||||
mImmutable = immutable;
|
||||
mMimeType = mimeType;
|
||||
mEncoding = encoding;
|
||||
mStatusCode = statusCode;
|
||||
mReasonPhrase = reasonPhrase;
|
||||
mResponseHeaders = responseHeaders;
|
||||
mInputStream = data;
|
||||
}
|
||||
|
||||
private void checkImmutable() {
|
||||
if (mImmutable)
|
||||
throw new IllegalStateException("This WebResourceResponse instance is immutable");
|
||||
}
|
||||
}
|
||||
|
||||
+61
-8
@@ -31,6 +31,7 @@ import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import android.view.textclassifier.TextClassifier
|
||||
import android.webkit.DownloadListener
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.RenderProcessGoneDetail
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebBackForwardList
|
||||
@@ -62,11 +63,13 @@ import org.cef.browser.CefFrame
|
||||
import org.cef.browser.CefMessageRouter
|
||||
import org.cef.browser.CefRendering
|
||||
import org.cef.callback.CefCallback
|
||||
import org.cef.callback.CefMediaAccessCallback
|
||||
import org.cef.callback.CefQueryCallback
|
||||
import org.cef.handler.CefDisplayHandlerAdapter
|
||||
import org.cef.handler.CefLoadHandler
|
||||
import org.cef.handler.CefLoadHandlerAdapter
|
||||
import org.cef.handler.CefMessageRouterHandlerAdapter
|
||||
import org.cef.handler.CefPermissionHandler
|
||||
import org.cef.handler.CefRequestHandler
|
||||
import org.cef.handler.CefRequestHandlerAdapter
|
||||
import org.cef.handler.CefResourceHandler
|
||||
@@ -167,6 +170,30 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private class CefPermissionRequest(
|
||||
private val url: String,
|
||||
private val permissionMask: Int,
|
||||
private val callback: CefMediaAccessCallback,
|
||||
) : PermissionRequest() {
|
||||
override fun getOrigin(): Uri = Uri.parse(url)
|
||||
|
||||
override fun getResources(): Array<String> {
|
||||
val retVal = mutableListOf<String>()
|
||||
if ((permissionMask and (1 shl 0)) > 0) retVal.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE)
|
||||
if ((permissionMask and (1 shl 1)) > 0) retVal.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
|
||||
return retVal.toTypedArray()
|
||||
}
|
||||
|
||||
override fun grant(resources: Array<String>) {
|
||||
// TODO: respect given resource grant
|
||||
callback.Continue(permissionMask)
|
||||
}
|
||||
|
||||
override fun deny() {
|
||||
callback.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
|
||||
override fun onConsoleMessage(
|
||||
browser: CefBrowser,
|
||||
@@ -363,7 +390,9 @@ class KcefWebViewProvider(
|
||||
Log.v(TAG, "Handling request from client's response for ${request.url}")
|
||||
try {
|
||||
resolvedData = webResponse.data.readAllBytes()
|
||||
Log.v(TAG, "Resolved client response for ${resolvedData?.size} bytes")
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to read client data", e)
|
||||
}
|
||||
callback.Continue()
|
||||
return true
|
||||
@@ -375,7 +404,7 @@ class KcefWebViewProvider(
|
||||
redirectUrl: StringRef,
|
||||
) {
|
||||
super.getResponseHeaders(response, responseLength, redirectUrl)
|
||||
webResponse.responseHeaders.forEach { response.setHeaderByName(it.key, it.value, true) }
|
||||
webResponse.responseHeaders?.forEach { response.setHeaderByName(it.key, it.value, true) }
|
||||
response.status = webResponse.statusCode
|
||||
response.mimeType = webResponse.mimeType
|
||||
}
|
||||
@@ -424,15 +453,22 @@ class KcefWebViewProvider(
|
||||
frame: CefFrame,
|
||||
request: CefRequest,
|
||||
): CefResourceHandler? {
|
||||
// TODO: we should be calling this on the handler, since CEF calls us on its IO thread
|
||||
val isInitialLoad = frame.url == "" && request.method == "GET"
|
||||
Log.v(TAG, "Request ${request.method} ${request.url} is initial? $isInitialLoad")
|
||||
// NOTE: we should be calling this on the handler, since CEF calls us on its IO thread
|
||||
// but docs say "This method is called on a thread other than the UI thread" so should be fine
|
||||
val response =
|
||||
viewClient.shouldInterceptRequest(
|
||||
view,
|
||||
CefWebResourceRequest(request, frame, false),
|
||||
)
|
||||
if (isInitialLoad) {
|
||||
null
|
||||
} else {
|
||||
viewClient.shouldInterceptRequest(
|
||||
view,
|
||||
CefWebResourceRequest(request, frame, false),
|
||||
)
|
||||
}
|
||||
if (response == null) {
|
||||
// prefer user's response override
|
||||
urlHttpMapping.get(request.url)?.let {
|
||||
urlHttpMapping.get(request.url.trimEnd('/'))?.let {
|
||||
return HtmlResponseResourceHandler(it)
|
||||
}
|
||||
}
|
||||
@@ -469,6 +505,22 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PermissionHandler : CefPermissionHandler {
|
||||
override fun onRequestMediaAccessPermission(
|
||||
browser: CefBrowser,
|
||||
frame: CefFrame,
|
||||
requesting_url: String,
|
||||
requested_permissions: Int,
|
||||
callback: CefMediaAccessCallback,
|
||||
): Boolean {
|
||||
handler.post {
|
||||
Log.v(TAG, "Checking permission for $requesting_url: $requested_permissions")
|
||||
chromeClient.onPermissionRequest(CefPermissionRequest(requesting_url, requested_permissions, callback))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun init(
|
||||
javaScriptInterfaces: Map<String, Any>?,
|
||||
privateBrowsing: Boolean,
|
||||
@@ -480,6 +532,7 @@ class KcefWebViewProvider(
|
||||
addDisplayHandler(DisplayHandler())
|
||||
addLoadHandler(LoadHandler())
|
||||
addRequestHandler(RequestHandler())
|
||||
addPermissionHandler(PermissionHandler())
|
||||
|
||||
val config = CefMessageRouter.CefMessageRouterConfig()
|
||||
config.jsQueryFunction = QUERY_FN
|
||||
@@ -615,7 +668,7 @@ class KcefWebViewProvider(
|
||||
browser =
|
||||
(
|
||||
baseUrl?.let { url ->
|
||||
urlHttpMapping.put(url, data)
|
||||
urlHttpMapping.put(url.trimEnd('/'), data)
|
||||
kcefClient!!.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
|
||||
Reference in New Issue
Block a user