blob: 88de335b7ac800819d9d5e0fbd23d5e004c5a6cc [file] [log] [blame]
/*
* Copyright 2018 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 androidx.webkit;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Looper;
import android.webkit.ValueCallback;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresFeature;
import androidx.core.os.BuildCompat;
import androidx.webkit.internal.WebViewFeatureInternal;
import androidx.webkit.internal.WebViewGlueCommunicator;
import androidx.webkit.internal.WebViewProviderAdapter;
import androidx.webkit.internal.WebViewProviderFactoryAdapter;
import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
/**
* Compatibility version of {@link android.webkit.WebView}
*/
public class WebViewCompat {
private WebViewCompat() {} // Don't allow instances of this class to be constructed.
/**
* Callback interface supplied to {@link #postVisualStateCallback} for receiving
* notifications about the visual state.
*/
public interface VisualStateCallback {
/**
* Invoked when the visual state is ready to be drawn in the next {@link WebView#onDraw}.
*
* @param requestId The identifier passed to {@link #postVisualStateCallback} when this
* callback was posted.
*/
void onComplete(long requestId);
}
/**
* Posts a {@link VisualStateCallback}, which will be called when
* the current state of the WebView is ready to be drawn.
*
* <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not
* immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The
* {@link VisualStateCallback} provides a mechanism to notify the caller when the contents
* of the DOM at the current time are ready to be drawn the next time the {@link WebView} draws.
*
* <p>The next draw after the callback completes is guaranteed to reflect all the updates to the
* DOM up to the point at which the {@link VisualStateCallback} was posted, but it may
* also contain updates applied after the callback was posted.
*
* <p>The state of the DOM covered by this API includes the following:
* <ul>
* <li>primitive HTML elements (div, img, span, etc..)</li>
* <li>images</li>
* <li>CSS animations</li>
* <li>WebGL</li>
* <li>canvas</li>
* </ul>
* It does not include the state of:
* <ul>
* <li>the video tag</li>
* </ul>
*
* <p>To guarantee that the {@link WebView} will successfully render the first frame
* after the {@link VisualStateCallback#onComplete} method has been called a set of
* conditions must be met:
* <ul>
* <li>If the {@link WebView}'s visibility is set to {@link android.view.View#VISIBLE VISIBLE}
* then * the {@link WebView} must be attached to the view hierarchy.</li>
* <li>If the {@link WebView}'s visibility is set to
* {@link android.view.View#INVISIBLE INVISIBLE} then the {@link WebView} must be attached to
* the view hierarchy and must be made {@link android.view.View#VISIBLE VISIBLE} from the
* {@link VisualStateCallback#onComplete} method.</li>
* <li>If the {@link WebView}'s visibility is set to {@link android.view.View#GONE GONE} then
* the {@link WebView} must be attached to the view hierarchy and its
* {@link android.widget.AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be
* set to fixed values and must be made {@link android.view.View#VISIBLE VISIBLE} from the
* {@link VisualStateCallback#onComplete} method.</li>
* </ul>
*
* <p>When using this API it is also recommended to enable pre-rasterization if the {@link
* WebView} is off screen to avoid flickering. See
* {@link android.webkit.WebSettings#setOffscreenPreRaster} for more details and do consider its
* caveats.
*
* This method should only be called if
* {@link WebViewFeature#isFeatureSupported(String)}
* returns true for {@link WebViewFeature#VISUAL_STATE_CALLBACK}.
*
* @param requestId An id that will be returned in the callback to allow callers to match
* requests with callbacks.
* @param callback The callback to be invoked.
*/
@SuppressWarnings("NewApi")
@RequiresFeature(name = WebViewFeature.VISUAL_STATE_CALLBACK,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void postVisualStateCallback(@NonNull WebView webview, long requestId,
@NonNull final VisualStateCallback callback) {
WebViewFeatureInternal webViewFeature =
WebViewFeatureInternal.getFeature(WebViewFeature.VISUAL_STATE_CALLBACK);
if (webViewFeature.isSupportedByFramework()) {
webview.postVisualStateCallback(requestId,
new android.webkit.WebView.VisualStateCallback() {
@Override
public void onComplete(long l) {
callback.onComplete(l);
}
});
} else if (webViewFeature.isSupportedByWebView()) {
checkThread(webview);
getProvider(webview).insertVisualStateCallback(requestId, callback);
} else {
WebViewFeatureInternal.throwUnsupportedOperationException("postVisualStateCallback");
}
}
/**
* Starts Safe Browsing initialization.
* <p>
* URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is
* invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those
* devices {@code callback} will receive {@code false}.
* <p>
* This should not be called if Safe Browsing has been disabled by manifest tag or {@link
* android.webkit.WebSettings#setSafeBrowsingEnabled}. This prepares resources used for Safe
* Browsing.
* <p>
* This should be called with the Application Context (and will always use the Application
* context to do its work regardless).
*
* @param context Application Context.
* @param callback will be called on the UI thread with {@code true} if initialization is
* successful, {@code false} otherwise.
*/
public static void startSafeBrowsing(@NonNull Context context,
@Nullable ValueCallback<Boolean> callback) {
if (Build.VERSION.SDK_INT >= 27) {
WebView.startSafeBrowsing(context, callback);
} else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
getFactory().getStatics().initSafeBrowsing(context, callback);
}
}
/**
* Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks.
* The list is global for all the WebViews.
* <p>
* Each rule should take one of these:
* <table>
* <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr>
* <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr>
* <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr>
* <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr>
* <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr>
* </table>
* <p>
* All other rules, including wildcards, are invalid.
* <p>
* The correct syntax for hosts is defined by <a
* href="https://tools.ietf.org/html/rfc3986#section-3.2.2">RFC 3986</a>.
*
* @param hosts the list of hosts
* @param callback will be called with {@code true} if hosts are successfully added to the
* whitelist. It will be called with {@code false} if any hosts are malformed. The callback
* will be run on the UI thread
*/
public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts,
@Nullable ValueCallback<Boolean> callback) {
if (Build.VERSION.SDK_INT >= 27) {
WebView.setSafeBrowsingWhitelist(hosts, callback);
} else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback);
}
}
/**
* Returns a URL pointing to the privacy policy for Safe Browsing reporting.
*
* @return the url pointing to a privacy policy document which can be displayed to users.
*/
@NonNull
public static Uri getSafeBrowsingPrivacyPolicyUrl() {
if (Build.VERSION.SDK_INT >= 27) {
return WebView.getSafeBrowsingPrivacyPolicyUrl();
} else { // TODO(gsennton): guard with WebViewApk.hasFeature(SafeBrowsing)
return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
}
}
/**
* If WebView has already been loaded into the current process this method will return the
* package that was used to load it. Otherwise, the package that would be used if the WebView
* was loaded right now will be returned; this does not cause WebView to be loaded, so this
* information may become outdated at any time.
* The WebView package changes either when the current WebView package is updated, disabled, or
* uninstalled. It can also be changed through a Developer Setting.
* If the WebView package changes, any app process that has loaded WebView will be killed. The
* next time the app starts and loads WebView it will use the new WebView package instead.
* @return the current WebView package, or {@code null} if there is none.
*/
// Note that this API is not protected by a {@link androidx.webkit.WebViewFeature} since
// this feature is not dependent on the WebView APK.
@Nullable
public static PackageInfo getCurrentWebViewPackage(@NonNull Context context) {
// There was no WebView Package before Lollipop, the WebView code was part of the framework
// back then.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return null;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return WebView.getCurrentWebViewPackage();
} else { // L-N
try {
PackageInfo loadedWebViewPackageInfo = getLoadedWebViewPackageInfo();
if (loadedWebViewPackageInfo != null) return loadedWebViewPackageInfo;
} catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException
| NoSuchMethodException e) {
return null;
}
// If WebViewFactory.getLoadedPackageInfo() returns null then WebView hasn't been loaded
// yet, in that case we need to fetch the name of the WebView package, and fetch the
// corresponding PackageInfo through the PackageManager
return getNotYetLoadedWebViewPackageInfo(context);
}
}
/**
* Return the PackageInfo of the currently loaded WebView APK. This method uses reflection and
* propagates any exceptions thrown, to the caller.
*/
private static PackageInfo getLoadedWebViewPackageInfo()
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Class webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
PackageInfo webviewPackageInfo =
(PackageInfo) webViewFactoryClass.getMethod(
"getLoadedPackageInfo").invoke(null);
return webviewPackageInfo;
}
/**
* Return the PackageInfo of the WebView APK that would have been used as WebView implementation
* if WebView was to be loaded right now.
*/
private static PackageInfo getNotYetLoadedWebViewPackageInfo(Context context) {
String webviewPackageName = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
Class webViewFactoryClass = null;
webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
webviewPackageName = (String) webViewFactoryClass.getMethod(
"getWebViewPackageName").invoke(null);
} else {
Class webviewUpdateServiceClass =
Class.forName("android.webkit.WebViewUpdateService");
webviewPackageName = (String) webviewUpdateServiceClass.getMethod(
"getCurrentWebViewPackageName").invoke(null);
}
} catch (ClassNotFoundException e) {
return null;
} catch (IllegalAccessException e) {
return null;
} catch (InvocationTargetException e) {
return null;
} catch (NoSuchMethodException e) {
return null;
}
if (webviewPackageName == null) return null;
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(webviewPackageName, 0);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
private static WebViewProviderAdapter getProvider(WebView webview) {
return new WebViewProviderAdapter(createProvider(webview));
}
private static WebViewProviderFactoryAdapter getFactory() {
return WebViewGlueCommunicator.getFactory();
}
private static WebViewProviderBoundaryInterface createProvider(WebView webview) {
return getFactory().createWebView(webview);
}
@SuppressWarnings("NewApi")
private static void checkThread(WebView webview) {
if (BuildCompat.isAtLeastP()) {
if (webview.getWebViewLooper() != Looper.myLooper()) {
throw new RuntimeException("A WebView method was called on thread '"
+ Thread.currentThread().getName() + "'. "
+ "All WebView methods must be called on the same thread. "
+ "(Expected Looper " + webview.getLooper() + " called on "
+ Looper.myLooper() + ", FYI main Looper is " + Looper.getMainLooper()
+ ")");
}
} else {
try {
Method checkThreadMethod = WebView.class.getDeclaredMethod("checkThread");
checkThreadMethod.setAccessible(true);
// WebView.checkThread() performs some logging and potentially throws an exception
// if WebView is used on the wrong thread.
checkThreadMethod.invoke(webview);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}