Home | History | Annotate | Download | only in webkit
      1 /*
      2  * Copyright 2018 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 
     17 package androidx.webkit;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.content.Context;
     21 import android.content.pm.PackageInfo;
     22 import android.content.pm.PackageManager;
     23 import android.net.Uri;
     24 import android.os.Build;
     25 import android.os.Looper;
     26 import android.webkit.ValueCallback;
     27 import android.webkit.WebView;
     28 
     29 import androidx.annotation.NonNull;
     30 import androidx.annotation.Nullable;
     31 import androidx.annotation.RequiresFeature;
     32 import androidx.core.os.BuildCompat;
     33 import androidx.webkit.internal.WebMessagePortImpl;
     34 import androidx.webkit.internal.WebViewFeatureInternal;
     35 import androidx.webkit.internal.WebViewGlueCommunicator;
     36 import androidx.webkit.internal.WebViewProviderAdapter;
     37 import androidx.webkit.internal.WebViewProviderFactory;
     38 
     39 import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
     40 
     41 import java.lang.reflect.InvocationTargetException;
     42 import java.lang.reflect.Method;
     43 import java.util.List;
     44 
     45 /**
     46  * Compatibility version of {@link android.webkit.WebView}
     47  */
     48 public class WebViewCompat {
     49     private static final Uri WILDCARD_URI = Uri.parse("*");
     50     private static final Uri EMPTY_URI = Uri.parse("");
     51 
     52     private WebViewCompat() {} // Don't allow instances of this class to be constructed.
     53 
     54     /**
     55      * Callback interface supplied to {@link #postVisualStateCallback} for receiving
     56      * notifications about the visual state.
     57      */
     58     public interface VisualStateCallback {
     59         /**
     60          * Invoked when the visual state is ready to be drawn in the next {@link WebView#onDraw}.
     61          *
     62          * @param requestId The identifier passed to {@link #postVisualStateCallback} when this
     63          *                  callback was posted.
     64          */
     65         void onComplete(long requestId);
     66     }
     67 
     68     /**
     69      * Posts a {@link VisualStateCallback}, which will be called when
     70      * the current state of the WebView is ready to be drawn.
     71      *
     72      * <p>Because updates to the DOM are processed asynchronously, updates to the DOM may not
     73      * immediately be reflected visually by subsequent {@link WebView#onDraw} invocations. The
     74      * {@link VisualStateCallback} provides a mechanism to notify the caller when the contents
     75      * of the DOM at the current time are ready to be drawn the next time the {@link WebView} draws.
     76      *
     77      * <p>The next draw after the callback completes is guaranteed to reflect all the updates to the
     78      * DOM up to the point at which the {@link VisualStateCallback} was posted, but it may
     79      * also contain updates applied after the callback was posted.
     80      *
     81      * <p>The state of the DOM covered by this API includes the following:
     82      * <ul>
     83      * <li>primitive HTML elements (div, img, span, etc..)</li>
     84      * <li>images</li>
     85      * <li>CSS animations</li>
     86      * <li>WebGL</li>
     87      * <li>canvas</li>
     88      * </ul>
     89      * It does not include the state of:
     90      * <ul>
     91      * <li>the video tag</li>
     92      * </ul>
     93      *
     94      * <p>To guarantee that the {@link WebView} will successfully render the first frame
     95      * after the {@link VisualStateCallback#onComplete} method has been called a set of
     96      * conditions must be met:
     97      * <ul>
     98      * <li>If the {@link WebView}'s visibility is set to {@link android.view.View#VISIBLE VISIBLE}
     99      * then * the {@link WebView} must be attached to the view hierarchy.</li>
    100      * <li>If the {@link WebView}'s visibility is set to
    101      * {@link android.view.View#INVISIBLE INVISIBLE} then the {@link WebView} must be attached to
    102      * the view hierarchy and must be made {@link android.view.View#VISIBLE VISIBLE} from the
    103      * {@link VisualStateCallback#onComplete} method.</li>
    104      * <li>If the {@link WebView}'s visibility is set to {@link android.view.View#GONE GONE} then
    105      * the {@link WebView} must be attached to the view hierarchy and its
    106      * {@link android.widget.AbsoluteLayout.LayoutParams LayoutParams}'s width and height need to be
    107      * set to fixed values and must be made {@link android.view.View#VISIBLE VISIBLE} from the
    108      * {@link VisualStateCallback#onComplete} method.</li>
    109      * </ul>
    110      *
    111      * <p>When using this API it is also recommended to enable pre-rasterization if the {@link
    112      * WebView} is off screen to avoid flickering. See
    113      * {@link android.webkit.WebSettings#setOffscreenPreRaster} for more details and do consider its
    114      * caveats.
    115      *
    116      * This method should only be called if
    117      * {@link WebViewFeature#isFeatureSupported(String)}
    118      * returns true for {@link WebViewFeature#VISUAL_STATE_CALLBACK}.
    119      *
    120      * @param requestId An id that will be returned in the callback to allow callers to match
    121      *                  requests with callbacks.
    122      * @param callback  The callback to be invoked.
    123      */
    124     @SuppressWarnings("NewApi")
    125     @RequiresFeature(name = WebViewFeature.VISUAL_STATE_CALLBACK,
    126             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
    127     public static void postVisualStateCallback(@NonNull WebView webview, long requestId,
    128             @NonNull final VisualStateCallback callback) {
    129         WebViewFeatureInternal webViewFeature =
    130                 WebViewFeatureInternal.getFeature(WebViewFeature.VISUAL_STATE_CALLBACK);
    131         if (webViewFeature.isSupportedByFramework()) {
    132             webview.postVisualStateCallback(requestId,
    133                     new android.webkit.WebView.VisualStateCallback() {
    134                         @Override
    135                         public void onComplete(long l) {
    136                             callback.onComplete(l);
    137                         }
    138                     });
    139         } else if (webViewFeature.isSupportedByWebView()) {
    140             checkThread(webview);
    141             getProvider(webview).insertVisualStateCallback(requestId, callback);
    142         } else {
    143             throw WebViewFeatureInternal.getUnsupportedOperationException();
    144         }
    145     }
    146 
    147     /**
    148      * Starts Safe Browsing initialization.
    149      * <p>
    150      * URL loads are not guaranteed to be protected by Safe Browsing until after {@code callback} is
    151      * invoked with {@code true}. Safe Browsing is not fully supported on all devices. For those
    152      * devices {@code callback} will receive {@code false}.
    153      * <p>
    154      * This should not be called if Safe Browsing has been disabled by manifest tag or {@link
    155      * android.webkit.WebSettings#setSafeBrowsingEnabled}. This prepares resources used for Safe
    156      * Browsing.
    157      * <p>
    158      * This should be called with the Application Context (and will always use the Application
    159      * context to do its work regardless).
    160      *
    161      * @param context Application Context.
    162      * @param callback will be called on the UI thread with {@code true} if initialization is
    163      * successful, {@code false} otherwise.
    164      */
    165     @SuppressLint("NewApi")
    166     @RequiresFeature(name = WebViewFeature.START_SAFE_BROWSING,
    167             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
    168     public static void startSafeBrowsing(@NonNull Context context,
    169             @Nullable ValueCallback<Boolean> callback) {
    170         WebViewFeatureInternal webviewFeature =
    171                 WebViewFeatureInternal.getFeature(WebViewFeature.START_SAFE_BROWSING);
    172         if (webviewFeature.isSupportedByFramework()) {
    173             WebView.startSafeBrowsing(context, callback);
    174         } else if (webviewFeature.isSupportedByWebView()) {
    175             getFactory().getStatics().initSafeBrowsing(context, callback);
    176         } else {
    177             throw WebViewFeatureInternal.getUnsupportedOperationException();
    178         }
    179     }
    180 
    181     /**
    182      * Sets the list of hosts (domain names/IP addresses) that are exempt from SafeBrowsing checks.
    183      * The list is global for all the WebViews.
    184      * <p>
    185      * Each rule should take one of these:
    186      * <table>
    187      * <tr><th> Rule </th> <th> Example </th> <th> Matches Subdomain</th> </tr>
    188      * <tr><td> HOSTNAME </td> <td> example.com </td> <td> Yes </td> </tr>
    189      * <tr><td> .HOSTNAME </td> <td> .example.com </td> <td> No </td> </tr>
    190      * <tr><td> IPV4_LITERAL </td> <td> 192.168.1.1 </td> <td> No </td></tr>
    191      * <tr><td> IPV6_LITERAL_WITH_BRACKETS </td><td>[10:20:30:40:50:60:70:80]</td><td>No</td></tr>
    192      * </table>
    193      * <p>
    194      * All other rules, including wildcards, are invalid.
    195      * <p>
    196      * The correct syntax for hosts is defined by <a
    197      * href="https://tools.ietf.org/html/rfc3986#section-3.2.2">RFC 3986</a>.
    198      *
    199      * @param hosts the list of hosts
    200      * @param callback will be called with {@code true} if hosts are successfully added to the
    201      * whitelist. It will be called with {@code false} if any hosts are malformed. The callback
    202      * will be run on the UI thread
    203      */
    204     @SuppressLint("NewApi")
    205     @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_WHITELIST,
    206             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
    207     public static void setSafeBrowsingWhitelist(@NonNull List<String> hosts,
    208             @Nullable ValueCallback<Boolean> callback) {
    209         WebViewFeatureInternal webviewFeature =
    210                 WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_WHITELIST);
    211         if (webviewFeature.isSupportedByFramework()) {
    212             WebView.setSafeBrowsingWhitelist(hosts, callback);
    213         } else if (webviewFeature.isSupportedByWebView()) {
    214             getFactory().getStatics().setSafeBrowsingWhitelist(hosts, callback);
    215         } else {
    216             throw WebViewFeatureInternal.getUnsupportedOperationException();
    217         }
    218     }
    219 
    220     /**
    221      * Returns a URL pointing to the privacy policy for Safe Browsing reporting.
    222      *
    223      * @return the url pointing to a privacy policy document which can be displayed to users.
    224      */
    225     @SuppressLint("NewApi")
    226     @NonNull
    227     @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL,
    228             enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
    229     public static Uri getSafeBrowsingPrivacyPolicyUrl() {
    230         WebViewFeatureInternal webviewFeature =
    231                 WebViewFeatureInternal.getFeature(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL);
    232         if (webviewFeature.isSupportedByFramework()) {
    233             return WebView.getSafeBrowsingPrivacyPolicyUrl();
    234         } else if (webviewFeature.isSupportedByWebView()) {
    235             return getFactory().getStatics().getSafeBrowsingPrivacyPolicyUrl();
    236         } else {
    237             throw WebViewFeatureInternal.getUnsupportedOperationException();
    238         }
    239     }
    240 
    241     /**
    242      * If WebView has already been loaded into the current process this method will return the
    243      * package that was used to load it. Otherwise, the package that would be used if the WebView
    244      * was loaded right now will be returned; this does not cause WebView to be loaded, so this
    245      * information may become outdated at any time.
    246      * The WebView package changes either when the current WebView package is updated, disabled, or
    247      * uninstalled. It can also be changed through a Developer Setting.
    248      * If the WebView package changes, any app process that has loaded WebView will be killed. The
    249      * next time the app starts and loads WebView it will use the new WebView package instead.
    250      * @return the current WebView package, or {@code null} if there is none.
    251      */
    252     // Note that this API is not protected by a {@link androidx.webkit.WebViewFeature} since
    253     // this feature is not dependent on the WebView APK.
    254     @Nullable
    255     public static PackageInfo getCurrentWebViewPackage(@NonNull Context context) {
    256         // There was no WebView Package before Lollipop, the WebView code was part of the framework
    257         // back then.
    258         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
    259             return null;
    260         }
    261 
    262         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    263             return WebView.getCurrentWebViewPackage();
    264         } else { // L-N
    265             try {
    266                 PackageInfo loadedWebViewPackageInfo = getLoadedWebViewPackageInfo();
    267                 if (loadedWebViewPackageInfo != null) return loadedWebViewPackageInfo;
    268             } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException
    269                 | NoSuchMethodException  e) {
    270                 return null;
    271             }
    272 
    273             // If WebViewFactory.getLoadedPackageInfo() returns null then WebView hasn't been loaded
    274             // yet, in that case we need to fetch the name of the WebView package, and fetch the
    275             // corresponding PackageInfo through the PackageManager
    276             return getNotYetLoadedWebViewPackageInfo(context);
    277         }
    278     }
    279 
    280     /**
    281      * Return the PackageInfo of the currently loaded WebView APK. This method uses reflection and
    282      * propagates any exceptions thrown, to the caller.
    283      */
    284     private static PackageInfo getLoadedWebViewPackageInfo()
    285             throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
    286             IllegalAccessException {
    287         Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
    288         PackageInfo webviewPackageInfo =
    289                 (PackageInfo) webViewFactoryClass.getMethod(
    290                         "getLoadedPackageInfo").invoke(null);
    291         return webviewPackageInfo;
    292     }
    293 
    294     /**
    295      * Return the PackageInfo of the WebView APK that would have been used as WebView implementation
    296      * if WebView was to be loaded right now.
    297      */
    298     private static PackageInfo getNotYetLoadedWebViewPackageInfo(Context context) {
    299         String webviewPackageName = null;
    300         try {
    301             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
    302                     && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
    303                 Class<?> webViewFactoryClass = null;
    304                 webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
    305 
    306                 webviewPackageName = (String) webViewFactoryClass.getMethod(
    307                         "getWebViewPackageName").invoke(null);
    308             } else {
    309                 Class<?> webviewUpdateServiceClass =
    310                         Class.forName("android.webkit.WebViewUpdateService");
    311                 webviewPackageName = (String) webviewUpdateServiceClass.getMethod(
    312                         "getCurrentWebViewPackageName").invoke(null);
    313             }
    314         } catch (ClassNotFoundException e) {
    315             return null;
    316         } catch (IllegalAccessException e) {
    317             return null;
    318         } catch (InvocationTargetException e) {
    319             return null;
    320         } catch (NoSuchMethodException e) {
    321             return null;
    322         }
    323         if (webviewPackageName == null) return null;
    324         PackageManager pm = context.getPackageManager();
    325         try {
    326             return pm.getPackageInfo(webviewPackageName, 0);
    327         } catch (PackageManager.NameNotFoundException e) {
    328             return null;
    329         }
    330     }
    331 
    332     private static WebViewProviderAdapter getProvider(WebView webview) {
    333         return new WebViewProviderAdapter(createProvider(webview));
    334     }
    335 
    336     /**
    337      * Creates a message channel to communicate with JS and returns the message
    338      * ports that represent the endpoints of this message channel. The HTML5 message
    339      * channel functionality is described
    340      * <a href="https://html.spec.whatwg.org/multipage/comms.html#messagechannel">here
    341      * </a>
    342      *
    343      * <p>The returned message channels are entangled and already in started state.
    344      *
    345      * @return an array of size two, containing the two message ports that form the message channel.
    346      */
    347     public static @NonNull WebMessagePortCompat[] createWebMessageChannel(
    348             @NonNull WebView webview) {
    349         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    350             return WebMessagePortImpl.portsToCompat(webview.createWebMessageChannel());
    351         } else { // TODO(gsennton) add reflection-based implementation
    352             throw WebViewFeatureInternal.getUnsupportedOperationException();
    353         }
    354     }
    355 
    356     /**
    357      * Post a message to main frame. The embedded application can restrict the
    358      * messages to a certain target origin. See
    359      * <a href="https://html.spec.whatwg.org/multipage/comms.html#posting-messages">
    360      * HTML5 spec</a> for how target origin can be used.
    361      * <p>
    362      * A target origin can be set as a wildcard ("*"). However this is not recommended.
    363      * See the page above for security issues.
    364      *
    365      * @param message the WebMessage
    366      * @param targetOrigin the target origin.
    367      */
    368     public static void postWebMessage(@NonNull WebView webview, @NonNull WebMessageCompat message,
    369             @NonNull Uri targetOrigin) {
    370         // The wildcard ("*") Uri was first supported in WebView 60, see
    371         // crrev/5ec5b67cbab33cea51b0ee11a286c885c2de4d5d, so on some Android versions using "*"
    372         // won't work. WebView has always supported using an empty Uri "" as a wildcard - so convert
    373         // "*" into "" here.
    374         if (WILDCARD_URI.equals(targetOrigin)) {
    375             targetOrigin = EMPTY_URI;
    376         }
    377 
    378         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    379             webview.postWebMessage(
    380                     WebMessagePortImpl.compatToFrameworkMessage(message),
    381                     targetOrigin);
    382         } else { // TODO(gsennton) add reflection-based implementation
    383             throw WebViewFeatureInternal.getUnsupportedOperationException();
    384         }
    385     }
    386 
    387     private static WebViewProviderFactory getFactory() {
    388         return WebViewGlueCommunicator.getFactory();
    389     }
    390 
    391     private static WebViewProviderBoundaryInterface createProvider(WebView webview) {
    392         return getFactory().createWebView(webview);
    393     }
    394 
    395     @SuppressWarnings("NewApi")
    396     private static void checkThread(WebView webview) {
    397         if (BuildCompat.isAtLeastP()) {
    398             if (webview.getWebViewLooper() != Looper.myLooper()) {
    399                 throw new RuntimeException("A WebView method was called on thread '"
    400                         + Thread.currentThread().getName() + "'. "
    401                         + "All WebView methods must be called on the same thread. "
    402                         + "(Expected Looper " + webview.getWebViewLooper() + " called on "
    403                         + Looper.myLooper() + ", FYI main Looper is " + Looper.getMainLooper()
    404                         + ")");
    405             }
    406         } else {
    407             try {
    408                 Method checkThreadMethod = WebView.class.getDeclaredMethod("checkThread");
    409                 checkThreadMethod.setAccessible(true);
    410                 // WebView.checkThread() performs some logging and potentially throws an exception
    411                 // if WebView is used on the wrong thread.
    412                 checkThreadMethod.invoke(webview);
    413             } catch (NoSuchMethodException e) {
    414                 throw new RuntimeException(e);
    415             } catch (IllegalAccessException e) {
    416                 throw new RuntimeException(e);
    417             } catch (InvocationTargetException e) {
    418                 throw new RuntimeException(e);
    419             }
    420         }
    421     }
    422 }
    423