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