Merge "Allow using isFeatureEnabled on APEX module init" into udc-dev am: c80be403c0

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/libs/net/+/23126677

Change-Id: I54612975355e9f6571d76e6e6b0b050272f8e4b9
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/common/device/com/android/net/module/util/DeviceConfigUtils.java b/common/device/com/android/net/module/util/DeviceConfigUtils.java
index f8f250d..dae4eb9 100644
--- a/common/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/common/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -16,9 +16,12 @@
 
 package com.android.net.module.util;
 
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
 import android.content.Context;
-import android.content.pm.ModuleInfo;
+import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.provider.DeviceConfig;
 import android.util.Log;
@@ -28,6 +31,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Utilities for modules to query {@link DeviceConfig} and flags.
  */
@@ -43,6 +49,11 @@
     public static final String TETHERING_MODULE_NAME = "com.android.tethering";
 
     @VisibleForTesting
+    public static final String RESOURCES_APK_INTENT =
+            "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
+    private static final String CONNECTIVITY_RES_PKG_DIR = "/apex/" + TETHERING_MODULE_NAME + "/";
+
+    @VisibleForTesting
     public static void resetPackageVersionCacheForTest() {
         sPackageVersion = -1;
         sModuleVersion = -1;
@@ -189,8 +200,13 @@
      */
     public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
             @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
+        // TODO: migrate callers to a non-generic isTetheringFeatureEnabled method.
+        if (!TETHERING_MODULE_NAME.equals(moduleName)) {
+            throw new IllegalArgumentException(
+                    "This method is only usable by the tethering module");
+        }
         try {
-            final long packageVersion = getModuleVersion(context, moduleName);
+            final long packageVersion = getTetheringModuleVersion(context);
             return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled);
         } catch (PackageManager.NameNotFoundException e) {
             Log.e(TAG, "Could not find the module name", e);
@@ -198,14 +214,6 @@
         }
     }
 
-    private static boolean maybeUseFixedPackageVersion(@NonNull Context context) {
-        final String packageName = context.getPackageName();
-        if (packageName == null) return false;
-
-        return packageName.equals("com.android.networkstack.tethering")
-                || packageName.equals("com.android.networkstack.tethering.inprocess");
-    }
-
     private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion,
             @NonNull String namespace, String name, boolean defaultEnabled)
             throws PackageManager.NameNotFoundException {
@@ -215,36 +223,40 @@
                 || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
     }
 
+    // Guess the tethering module name based on the package prefix of the connectivity resources
+    // Take the resource package name, cut it before "connectivity" and append "tethering".
+    // Then resolve that package version number with packageManager.
+    // If that fails retry by appending "go.tethering" instead
+    private static long resolveTetheringModuleVersion(@NonNull Context context)
+            throws PackageManager.NameNotFoundException {
+        final String connResourcesPackage = getConnectivityResourcesPackageName(context);
+        final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity");
+        if (pkgPrefixLen < 0) {
+            throw new IllegalStateException(
+                    "Invalid connectivity resources package: " + connResourcesPackage);
+        }
+
+        final String pkgPrefix = connResourcesPackage.substring(0, pkgPrefixLen);
+        final PackageManager packageManager = context.getPackageManager();
+        try {
+            return packageManager.getPackageInfo(pkgPrefix + "tethering",
+                    PackageManager.MATCH_APEX).getLongVersionCode();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Device is using go modules");
+            // fall through
+        }
+
+        return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
+                PackageManager.MATCH_APEX).getLongVersionCode();
+    }
+
     private static volatile long sModuleVersion = -1;
-    @VisibleForTesting public static long FIXED_PACKAGE_VERSION = 10;
-    private static long getModuleVersion(@NonNull Context context, @NonNull String moduleName)
+    private static long getTetheringModuleVersion(@NonNull Context context)
             throws PackageManager.NameNotFoundException {
         if (sModuleVersion >= 0) return sModuleVersion;
 
-        final PackageManager packageManager = context.getPackageManager();
-        ModuleInfo module;
-        try {
-            module = packageManager.getModuleInfo(
-                    moduleName, PackageManager.MODULE_APEX_NAME);
-        } catch (PackageManager.NameNotFoundException e) {
-            // The error may happen if mainline module meta data is not installed e.g. there are
-            // no meta data configuration in AOSP build. To be able to enable a feature in AOSP
-            // by setting a flag via ADB for example. set a small non-zero fixed number for
-            // comparing.
-            if (maybeUseFixedPackageVersion(context)) {
-                sModuleVersion = FIXED_PACKAGE_VERSION;
-                return FIXED_PACKAGE_VERSION;
-            } else {
-                throw e;
-            }
-        }
-        String modulePackageName = module.getPackageName();
-        if (modulePackageName == null) throw new PackageManager.NameNotFoundException(moduleName);
-        final long version = packageManager.getPackageInfo(modulePackageName,
-                PackageManager.MATCH_APEX).getLongVersionCode();
-        sModuleVersion = version;
-
-        return version;
+        sModuleVersion = resolveTetheringModuleVersion(context);
+        return sModuleVersion;
     }
 
     /**
@@ -272,4 +284,24 @@
             return defaultValue;
         }
     }
+
+    /**
+     * Get the package name of the ServiceConnectivityResources package, used to provide resources
+     * for service-connectivity.
+     */
+    @NonNull
+    public static String getConnectivityResourcesPackageName(@NonNull Context context) {
+        final List<ResolveInfo> pkgs = new ArrayList<>(context.getPackageManager()
+                .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY));
+        pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(
+                CONNECTIVITY_RES_PKG_DIR));
+        if (pkgs.size() > 1) {
+            Log.wtf(TAG, "More than one connectivity resources package found: " + pkgs);
+        }
+        if (pkgs.isEmpty()) {
+            throw new IllegalStateException("No connectivity resource package found");
+        }
+
+        return pkgs.get(0).activityInfo.applicationInfo.packageName;
+    }
 }
diff --git a/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index a8e7993..b75939b 100644
--- a/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/common/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -16,25 +16,30 @@
 
 package com.android.net.module.util;
 
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.net.module.util.DeviceConfigUtils.FIXED_PACKAGE_VERSION;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.Context;
-import android.content.pm.ModuleInfo;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.provider.DeviceConfig;
 
@@ -49,6 +54,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.MockitoSession;
 
+import java.util.Arrays;
+
 
 /**
  * Tests for DeviceConfigUtils.
@@ -66,14 +73,23 @@
     private static final int TEST_MIN_FLAG_VALUE = 100;
     private static final long TEST_PACKAGE_VERSION = 290000000;
     private static final String TEST_PACKAGE_NAME = "test.package.name";
-    private static final String TETHERING_AOSP_PACKAGE_NAME = "com.android.networkstack.tethering";
-    private static final String TEST_APEX_NAME = "test.apex.name";
+    // The APEX name is the name of the APEX module, as in android.content.pm.ModuleInfo, and is
+    // used for its mount point in /apex. APEX packages are actually APKs with a different
+    // file extension, so they have an AndroidManifest: the APEX package name is the package name in
+    // that manifest, and is reflected in android.content.pm.ApplicationInfo. Contrary to the APEX
+    // (module) name, different package names are typically used to identify the organization that
+    // built and signed the APEX modules.
+    private static final String TEST_APEX_NAME = "com.android.tethering";
+    private static final String TEST_APEX_PACKAGE_NAME = "com.prefix.android.tethering";
+    private static final String TEST_GO_APEX_PACKAGE_NAME = "com.prefix.android.go.tethering";
+    private static final String TEST_CONNRES_PACKAGE_NAME =
+            "com.prefix.android.connectivity.resources";
+    private final PackageInfo mPackageInfo = new PackageInfo();
+    private final PackageInfo mApexPackageInfo = new PackageInfo();
     private MockitoSession mSession;
 
     @Mock private Context mContext;
     @Mock private PackageManager mPm;
-    @Mock private ModuleInfo mMi;
-    @Mock private PackageInfo mPi;
     @Mock private Resources mResources;
 
     @Before
@@ -81,15 +97,26 @@
         MockitoAnnotations.initMocks(this);
         mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
 
-        final PackageInfo pi = new PackageInfo();
-        pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+        mPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+        mApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
 
         doReturn(mPm).when(mContext).getPackageManager();
         doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
-        doReturn(mMi).when(mPm).getModuleInfo(eq(TEST_APEX_NAME), anyInt());
-        doReturn(TEST_PACKAGE_NAME).when(mMi).getPackageName();
-        doReturn(pi).when(mPm).getPackageInfo(anyString(), anyInt());
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
+        doReturn(mPackageInfo).when(mPm).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
+        doReturn(mApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_APEX_PACKAGE_NAME), anyInt());
+
         doReturn(mResources).when(mContext).getResources();
+
+        final ResolveInfo ri = new ResolveInfo();
+        ri.activityInfo = new ActivityInfo();
+        ri.activityInfo.applicationInfo = new ApplicationInfo();
+        ri.activityInfo.applicationInfo.packageName = TEST_CONNRES_PACKAGE_NAME;
+        ri.activityInfo.applicationInfo.sourceDir =
+                "/apex/com.android.tethering/priv-app/ServiceConnectivityResources@version";
+        doReturn(Arrays.asList(ri)).when(mPm).queryIntentActivities(argThat(
+                intent -> intent.getAction().equals(DeviceConfigUtils.RESOURCES_APK_INTENT)),
+                eq(MATCH_SYSTEM_ONLY));
     }
 
     @After
@@ -223,35 +250,32 @@
                 TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
-        doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
-        assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
     }
 
-
     @Test
-    public void testFeatureIsEnabledUsingFixedVersion() throws Exception {
-        doReturn(TETHERING_AOSP_PACKAGE_NAME).when(mContext).getPackageName();
-        doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
-
-        doReturn(Long.toString(FIXED_PACKAGE_VERSION)).when(() -> DeviceConfig.getProperty(
+    public void testFeatureIsEnabledOnGo() throws Exception {
+        doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(
+                eq(TEST_APEX_PACKAGE_NAME), anyInt());
+        doReturn(mApexPackageInfo).when(mPm).getPackageInfo(
+                eq(TEST_GO_APEX_PACKAGE_NAME), anyInt());
+        doReturn("0").when(() -> DeviceConfig.getProperty(
                 eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
-        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
-                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
 
-        doReturn(Long.toString(FIXED_PACKAGE_VERSION + 1)).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG));
         assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
+        assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+                TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
 
-        doReturn(Long.toString(FIXED_PACKAGE_VERSION - 1)).when(() -> DeviceConfig.getProperty(
-                eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
     }
 
     @Test
-    public void testFeatureIsEnabledCaching() throws Exception {
+    public void testFeatureIsEnabledCaching_APK() throws Exception {
         doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
                 eq(TEST_EXPERIMENT_FLAG)));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
@@ -263,14 +287,20 @@
         verify(mContext, times(1)).getPackageManager();
         verify(mContext, times(1)).getPackageName();
         verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+    }
 
+    @Test
+    public void testFeatureIsEnabledCaching_APEX() throws Exception {
+        doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+                eq(TEST_EXPERIMENT_FLAG)));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
         assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
                 TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
 
-        // Module info is only queried once
-        verify(mPm, times(1)).getModuleInfo(anyString(), anyInt());
+        // Package info is only queried once
+        verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+        verify(mContext, never()).getPackageName();
     }
 
     @Test