/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.net.module.util; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.provider.DeviceConfig.NAMESPACE_TETHERING; import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID; import static com.android.net.module.util.FeatureVersions.MODULE_MASK; import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID; import static com.android.net.module.util.FeatureVersions.VERSION_MASK; import android.content.Context; 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; import androidx.annotation.BoolRes; import androidx.annotation.NonNull; 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. */ public final class DeviceConfigUtils { private DeviceConfigUtils() {} private static final String TAG = DeviceConfigUtils.class.getSimpleName(); /** * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value * until they are recompiled, so modifying this constant means that different modules may * be referencing a different tethering module variant, or having a stale reference. */ 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 final long DEFAULT_PACKAGE_VERSION = 1000; @VisibleForTesting public static void resetPackageVersionCacheForTest() { sPackageVersion = -1; sModuleVersion = -1; sNetworkStackModuleVersion = -1; } private static volatile long sPackageVersion = -1; private static long getPackageVersion(@NonNull final Context context) { // sPackageVersion may be set by another thread just after this check, but querying the // package version several times on rare occasions is fine. if (sPackageVersion >= 0) { return sPackageVersion; } try { final long version = context.getPackageManager().getPackageInfo( context.getPackageName(), 0).getLongVersionCode(); sPackageVersion = version; return version; } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Failed to get package info: " + e); return DEFAULT_PACKAGE_VERSION; } } /** * Look up the value of a property for a particular namespace from {@link DeviceConfig}. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @param defaultValue The value to return if the property does not exist or has no valid value. * @return the corresponding value, or defaultValue if none exists. */ @Nullable public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name, @Nullable String defaultValue) { String value = DeviceConfig.getProperty(namespace, name); return value != null ? value : defaultValue; } /** * Look up the value of a property for a particular namespace from {@link DeviceConfig}. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @param defaultValue The value to return if the property does not exist or its value is null. * @return the corresponding value, or defaultValue if none exists. */ public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, int defaultValue) { String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); try { return (value != null) ? Integer.parseInt(value) : defaultValue; } catch (NumberFormatException e) { return defaultValue; } } /** * Look up the value of a property for a particular namespace from {@link DeviceConfig}. * * Flags like timeouts should use this method and set an appropriate min/max range: if invalid * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range * protects against this kind of breakage. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @param minimumValue The minimum value of a property. * @param maximumValue The maximum value of a property. * @param defaultValue The value to return if the property does not exist or its value is null. * @return the corresponding value, or defaultValue if none exists or the fetched value is * not in the provided range. */ public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, int minimumValue, int maximumValue, int defaultValue) { int value = getDeviceConfigPropertyInt(namespace, name, defaultValue); if (value < minimumValue || value > maximumValue) return defaultValue; return value; } /** * Look up the value of a property for a particular namespace from {@link DeviceConfig}. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @param defaultValue The value to return if the property does not exist or its value is null. * @return the corresponding value, or defaultValue if none exists. */ public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace, @NonNull String name, boolean defaultValue) { String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); return (value != null) ? Boolean.parseBoolean(value) : defaultValue; } /** * Check whether or not one specific experimental feature for a particular namespace from * {@link DeviceConfig} is enabled by comparing module package version * with current version of property. If this property version is valid, the corresponding * experimental feature would be enabled, otherwise disabled. * * This is useful to ensure that if a module install is rolled back, flags are not left fully * rolled out on a version where they have not been well tested. * @param context The global context information about an app environment. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @return true if this feature is enabled, or false if disabled. */ public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, @NonNull String name) { return isFeatureEnabled(context, namespace, name, false /* defaultEnabled */); } /** * Check whether or not one specific experimental feature for a particular namespace from * {@link DeviceConfig} is enabled by comparing module package version * with current version of property. If this property version is valid, the corresponding * experimental feature would be enabled, otherwise disabled. * * This is useful to ensure that if a module install is rolled back, flags are not left fully * rolled out on a version where they have not been well tested. * @param context The global context information about an app environment. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @param defaultEnabled The value to return if the property does not exist or its value is * null. * @return true if this feature is enabled, or false if disabled. */ public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, @NonNull String name, boolean defaultEnabled) { final long packageVersion = getPackageVersion(context); return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); } /** * Check whether or not one specific experimental feature for a particular namespace from * {@link DeviceConfig} is enabled by comparing module package version * with current version of property. If this property version is valid, the corresponding * experimental feature would be enabled, otherwise disabled. * * This is useful to ensure that if a module install is rolled back, flags are not left fully * rolled out on a version where they have not been well tested. * @param context The global context information about an app environment. * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @param moduleName The mainline module name which is released as apex. * @param defaultEnabled The value to return if the property does not exist or its value is * null. * @return true if this feature is enabled, or false if disabled. */ public static boolean isTetheringFeatureEnabled(@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"); } final long packageVersion = getTetheringModuleVersion(context); return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); } private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion, @NonNull String namespace, String name, boolean defaultEnabled) { final int propertyVersion = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */); return (propertyVersion == 0 && defaultEnabled) || (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 pkgPrefix = resolvePkgPrefix(context); 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 String resolvePkgPrefix(Context context) { final String connResourcesPackage = getConnectivityResourcesPackageName(context); final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity"); if (pkgPrefixLen < 0) { throw new IllegalStateException( "Invalid connectivity resources package: " + connResourcesPackage); } return connResourcesPackage.substring(0, pkgPrefixLen); } private static volatile long sModuleVersion = -1; private static long getTetheringModuleVersion(@NonNull Context context) { if (sModuleVersion >= 0) return sModuleVersion; try { sModuleVersion = resolveTetheringModuleVersion(context); } catch (PackageManager.NameNotFoundException e) { // It's expected to fail tethering module version resolution on the devices with // flattened apex Log.e(TAG, "Failed to resolve tethering module version: " + e); return DEFAULT_PACKAGE_VERSION; } return sModuleVersion; } private static volatile long sNetworkStackModuleVersion = -1; /** * Get networkstack module version. */ @VisibleForTesting static long getNetworkStackModuleVersion(@NonNull Context context) { if (sNetworkStackModuleVersion >= 0) return sNetworkStackModuleVersion; try { sNetworkStackModuleVersion = resolveNetworkStackModuleVersion(context); } catch (PackageManager.NameNotFoundException e) { Log.wtf(TAG, "Failed to resolve networkstack module version: " + e); return DEFAULT_PACKAGE_VERSION; } return sNetworkStackModuleVersion; } private static long resolveNetworkStackModuleVersion(@NonNull Context context) throws PackageManager.NameNotFoundException { // TODO(b/293975546): Strictly speaking this is the prefix for connectivity and not // network stack. In practice, it's the same. Read the prefix from network stack instead. final String pkgPrefix = resolvePkgPrefix(context); final PackageManager packageManager = context.getPackageManager(); try { return packageManager.getPackageInfo(pkgPrefix + "networkstack", PackageManager.MATCH_SYSTEM_ONLY).getLongVersionCode(); } catch (PackageManager.NameNotFoundException e) { Log.d(TAG, "Device is using go or non-mainline modules"); // fall through } return packageManager.getPackageInfo(pkgPrefix + "go.networkstack", PackageManager.MATCH_ALL).getLongVersionCode(); } /** * Check whether one specific feature is supported from the feature Id. The feature Id is * composed by a module package Id and version Id from {@link FeatureVersions}. * * This is useful when a feature required minimal module version supported and cannot function * well with a standalone newer module. * @param context The global context information about an app environment. * @param featureId The feature id that contains required module id and minimal module version * @return true if this feature is supported, or false if not supported. **/ public static boolean isFeatureSupported(@NonNull Context context, long featureId) { final long moduleVersion; final long moduleId = featureId & MODULE_MASK; if (moduleId == CONNECTIVITY_MODULE_ID) { moduleVersion = getTetheringModuleVersion(context); } else if (moduleId == NETWORK_STACK_MODULE_ID) { moduleVersion = getNetworkStackModuleVersion(context); } else { throw new IllegalArgumentException("Unknown module " + moduleId); } // Support by default if no module version is available. return moduleVersion == DEFAULT_PACKAGE_VERSION || moduleVersion >= (featureId & VERSION_MASK); } /** * Check whether one specific experimental feature in specific namespace from * {@link DeviceConfig} is not disabled. Feature can be disabled by setting a non-zero * value in the property. If the feature is enabled by default and disabled by flag push * (kill switch), this method should be used. If the feature is disabled by default and * enabled by flag push, {@link #isFeatureEnabled} should be used. * * @param namespace The namespace containing the property to look up. * @param name The name of the property to look up. * @return true if this feature is enabled, or false if disabled. */ private static boolean isFeatureNotChickenedOut(String namespace, String name) { final int propertyVersion = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */); return propertyVersion == 0; } /** * Check whether one specific experimental feature in Tethering module from {@link DeviceConfig} * is not disabled. * * @param name The name of the property in tethering module to look up. * @return true if this feature is enabled, or false if disabled. */ public static boolean isTetheringFeatureNotChickenedOut(String name) { return isFeatureNotChickenedOut(NAMESPACE_TETHERING, name); } /** * Check whether one specific experimental feature in NetworkStack module from * {@link DeviceConfig} is not disabled. * * @param name The name of the property in NetworkStack module to look up. * @return true if this feature is enabled, or false if disabled. */ public static boolean isNetworkStackFeatureNotChickenedOut(String name) { return isFeatureNotChickenedOut(NAMESPACE_CONNECTIVITY, name); } /** * Gets boolean config from resources. */ public static boolean getResBooleanConfig(@NonNull final Context context, @BoolRes int configResource, final boolean defaultValue) { final Resources res = context.getResources(); try { return res.getBoolean(configResource); } catch (Resources.NotFoundException e) { return defaultValue; } } /** * Gets int config from resources. */ public static int getResIntegerConfig(@NonNull final Context context, @BoolRes int configResource, final int defaultValue) { final Resources res = context.getResources(); try { return res.getInteger(configResource); } catch (Resources.NotFoundException e) { 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 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; } }