[WebView Support Library] Create WebViewCompat.getCurrentWebViewPackage

WebViewCompat.getCurrentWebViewPackage is implemented differently for
different versions of Android - on Oreo and up we can use the existing
WebView.getCurrentWebViewPackage API, but for earlier Android versions
we need to use reflection to either fetch the currently loaded WebView
package, or the to-be-loaded WebView package.

Bug: 74100420
Test: run androidx.webkit tests.
Change-Id: Ia57221a792da3f468991d08d50426fc482cb0794
diff --git a/webkit/api/current.txt b/webkit/api/current.txt
index 36bc76b..fb15e2f 100644
--- a/webkit/api/current.txt
+++ b/webkit/api/current.txt
@@ -32,6 +32,7 @@
   }
 
   public class WebViewCompat {
+    method public static android.content.pm.PackageInfo getCurrentWebViewPackage(android.content.Context);
     method public static android.net.Uri getSafeBrowsingPrivacyPolicyUrl();
     method public static void postVisualStateCallback(android.webkit.WebView, long, androidx.webkit.WebViewCompat.VisualStateCallback);
     method public static void setSafeBrowsingWhitelist(java.util.List<java.lang.String>, android.webkit.ValueCallback<java.lang.Boolean>);
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
index 3c64b9c..c5f22f7 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewCompatTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -235,4 +236,22 @@
             Assert.fail("The privacy policy URL should be a well-formed URL");
         }
     }
+
+    /**
+     * WebViewCompat.getCurrentWebViewPackage should be null on pre-L devices.
+     * On L+ devices WebViewCompat.getCurrentWebViewPackage should be null only in exceptional
+     * circumstances - like when the WebView APK is being updated, or for Wear devices. The L+
+     * devices used in support library testing should have a non-null WebView package.
+     */
+    @Test
+    public void testGetCurrentWebViewPackage() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            assertNull(WebViewCompat.getCurrentWebViewPackage(
+                    InstrumentationRegistry.getTargetContext()));
+        } else {
+            assertNotNull(
+                    WebViewCompat.getCurrentWebViewPackage(
+                            InstrumentationRegistry.getTargetContext()));
+        }
+    }
 }
diff --git a/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index 0098a82..45def2e 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -17,6 +17,8 @@
 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;
@@ -210,6 +212,92 @@
         }
     }
 
+    /**
+     * 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 | IllegalAccessException | InvocationTargetException
+                | 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));
     }