blob: e646f37a6e2d5ad25e8bb9b1902a2c960a2918be [file] [log] [blame]
/*
* 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 name The name of the property to look up.
* @return true if this feature is enabled, or false if disabled.
*/
public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
@NonNull String name) {
return isNetworkStackFeatureEnabled(context, 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 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 isNetworkStackFeatureEnabled(@NonNull Context context,
@NonNull String name, boolean defaultEnabled) {
final long packageVersion = getPackageVersion(context);
return isFeatureEnabled(context, packageVersion, NAMESPACE_CONNECTIVITY, 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.
*
* If the feature is disabled by default and enabled by flag push, this method should be used.
* If the feature is enabled by default and disabled by flag push (kill switch),
* {@link #isTetheringFeatureNotChickenedOut(String)} should be used.
*
* @param context The global context information about an app environment.
* @param name The name of the property to look up.
* @return true if this feature is enabled, or false if disabled.
*/
public static boolean isTetheringFeatureEnabled(@NonNull Context context,
@NonNull String name) {
final long packageVersion = getTetheringModuleVersion(context);
return isFeatureEnabled(context, packageVersion, NAMESPACE_TETHERING, name,
false /* 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<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;
}
}