Allow using isFeatureEnabled on APEX module init
isFeatureEnabled is not usable in APEX module service initialization,
as it relies on PackageManager.getModuleInfo, which is only available
once the module controller is initialized. Also, there is no module
metadata in base AOSP.
Avoid this by guessing the tethering module name based on its
APK-in-APEX packages, which can be queried at that time.
ConnectivityServiceResources will indicate the package prefix used on
the device.
If querying with prefix + "tethering" fails, retry querying with prefix
+ "go.tethering" for Go devices.
This should allow using isFeatureEnabled in services (like
ConnectivityService) constructors, and allows removing the
FIXED_PACKAGE_VERSION hack when Tethering is not considered a module.
getConnectivityResourcesPackageName is implemented based on existing
code in ConnectivityResources.java. aosp/2446026 updates
ConnectivityResources to use getConnectivityResourcesPackageName.
Bug: 279108992
Test: atest
Change-Id: I277f4583e92ba41d53bd19666f1e8e29f68dfcd1
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index f8f250d..dae4eb9 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/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/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index a8e7993..b75939b 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/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