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
17package androidx.webkit;
18
19import android.annotation.SuppressLint;
20import android.content.Context;
21import android.content.pm.PackageInfo;
22import android.content.pm.PackageManager;
23import android.net.Uri;
24import android.os.Build;
25import android.os.Looper;
26import android.webkit.ValueCallback;
27import android.webkit.WebView;
28
29import androidx.annotation.NonNull;
30import androidx.annotation.Nullable;
31import androidx.annotation.RequiresFeature;
32import androidx.core.os.BuildCompat;
33import androidx.webkit.internal.WebMessagePortImpl;
34import androidx.webkit.internal.WebViewFeatureInternal;
35import androidx.webkit.internal.WebViewGlueCommunicator;
36import androidx.webkit.internal.WebViewProviderAdapter;
37import androidx.webkit.internal.WebViewProviderFactory;
38
39import org.chromium.support_lib_boundary.WebViewProviderBoundaryInterface;
40
41import java.lang.reflect.InvocationTargetException;
42import java.lang.reflect.Method;
43import java.util.List;
44
45/**
46 * Compatibility version of {@link android.webkit.WebView}
47 */
48public 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