/* * 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 android.content.Context; import android.content.pm.ModuleInfo; import android.content.pm.PackageManager; 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; /** * 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 void resetPackageVersionCacheForTest() { sPackageVersion = -1; sModuleVersion = -1; } private static volatile long sPackageVersion = -1; private static long getPackageVersion(@NonNull final Context context) throws PackageManager.NameNotFoundException { // 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; } final long version = context.getPackageManager().getPackageInfo( context.getPackageName(), 0).getLongVersionCode(); sPackageVersion = version; return 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) { try { final long packageVersion = getPackageVersion(context); return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Could not find the package name", e); return false; } } /** * 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 isFeatureEnabled(@NonNull Context context, @NonNull String namespace, @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) { try { final long packageVersion = getModuleVersion(context, moduleName); return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Could not find the module name", e); return false; } } 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 { final int propertyVersion = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */); return (propertyVersion == 0 && defaultEnabled) || (propertyVersion != 0 && packageVersion >= (long) propertyVersion); } private static volatile long sModuleVersion = -1; @VisibleForTesting public static long FIXED_PACKAGE_VERSION = 10; private static long getModuleVersion(@NonNull Context context, @NonNull String moduleName) 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; } /** * 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; } } }