1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.net.module.util; 18 19 import android.content.Context; 20 import android.content.pm.ModuleInfo; 21 import android.content.pm.PackageManager; 22 import android.content.res.Resources; 23 import android.provider.DeviceConfig; 24 import android.util.Log; 25 26 import androidx.annotation.BoolRes; 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.annotation.VisibleForTesting; 30 31 /** 32 * Utilities for modules to query {@link DeviceConfig} and flags. 33 */ 34 public final class DeviceConfigUtils { DeviceConfigUtils()35 private DeviceConfigUtils() {} 36 37 private static final String TAG = DeviceConfigUtils.class.getSimpleName(); 38 /** 39 * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value 40 * until they are recompiled, so modifying this constant means that different modules may 41 * be referencing a different tethering module variant, or having a stale reference. 42 */ 43 public static final String TETHERING_MODULE_NAME = "com.android.tethering"; 44 45 @VisibleForTesting resetPackageVersionCacheForTest()46 public static void resetPackageVersionCacheForTest() { 47 sPackageVersion = -1; 48 sModuleVersion = -1; 49 } 50 51 private static volatile long sPackageVersion = -1; getPackageVersion(@onNull final Context context)52 private static long getPackageVersion(@NonNull final Context context) 53 throws PackageManager.NameNotFoundException { 54 // sPackageVersion may be set by another thread just after this check, but querying the 55 // package version several times on rare occasions is fine. 56 if (sPackageVersion >= 0) { 57 return sPackageVersion; 58 } 59 final long version = context.getPackageManager().getPackageInfo( 60 context.getPackageName(), 0).getLongVersionCode(); 61 sPackageVersion = version; 62 return version; 63 } 64 65 /** 66 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 67 * @param namespace The namespace containing the property to look up. 68 * @param name The name of the property to look up. 69 * @param defaultValue The value to return if the property does not exist or has no valid value. 70 * @return the corresponding value, or defaultValue if none exists. 71 */ 72 @Nullable getDeviceConfigProperty(@onNull String namespace, @NonNull String name, @Nullable String defaultValue)73 public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name, 74 @Nullable String defaultValue) { 75 String value = DeviceConfig.getProperty(namespace, name); 76 return value != null ? value : defaultValue; 77 } 78 79 /** 80 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 81 * @param namespace The namespace containing the property to look up. 82 * @param name The name of the property to look up. 83 * @param defaultValue The value to return if the property does not exist or its value is null. 84 * @return the corresponding value, or defaultValue if none exists. 85 */ getDeviceConfigPropertyInt(@onNull String namespace, @NonNull String name, int defaultValue)86 public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, 87 int defaultValue) { 88 String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); 89 try { 90 return (value != null) ? Integer.parseInt(value) : defaultValue; 91 } catch (NumberFormatException e) { 92 return defaultValue; 93 } 94 } 95 96 /** 97 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 98 * 99 * Flags like timeouts should use this method and set an appropriate min/max range: if invalid 100 * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range 101 * protects against this kind of breakage. 102 * @param namespace The namespace containing the property to look up. 103 * @param name The name of the property to look up. 104 * @param minimumValue The minimum value of a property. 105 * @param maximumValue The maximum value of a property. 106 * @param defaultValue The value to return if the property does not exist or its value is null. 107 * @return the corresponding value, or defaultValue if none exists or the fetched value is 108 * not in the provided range. 109 */ getDeviceConfigPropertyInt(@onNull String namespace, @NonNull String name, int minimumValue, int maximumValue, int defaultValue)110 public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, 111 int minimumValue, int maximumValue, int defaultValue) { 112 int value = getDeviceConfigPropertyInt(namespace, name, defaultValue); 113 if (value < minimumValue || value > maximumValue) return defaultValue; 114 return value; 115 } 116 117 /** 118 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 119 * @param namespace The namespace containing the property to look up. 120 * @param name The name of the property to look up. 121 * @param defaultValue The value to return if the property does not exist or its value is null. 122 * @return the corresponding value, or defaultValue if none exists. 123 */ getDeviceConfigPropertyBoolean(@onNull String namespace, @NonNull String name, boolean defaultValue)124 public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace, 125 @NonNull String name, boolean defaultValue) { 126 String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); 127 return (value != null) ? Boolean.parseBoolean(value) : defaultValue; 128 } 129 130 /** 131 * Check whether or not one specific experimental feature for a particular namespace from 132 * {@link DeviceConfig} is enabled by comparing module package version 133 * with current version of property. If this property version is valid, the corresponding 134 * experimental feature would be enabled, otherwise disabled. 135 * 136 * This is useful to ensure that if a module install is rolled back, flags are not left fully 137 * rolled out on a version where they have not been well tested. 138 * @param context The global context information about an app environment. 139 * @param namespace The namespace containing the property to look up. 140 * @param name The name of the property to look up. 141 * @return true if this feature is enabled, or false if disabled. 142 */ isFeatureEnabled(@onNull Context context, @NonNull String namespace, @NonNull String name)143 public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, 144 @NonNull String name) { 145 return isFeatureEnabled(context, namespace, name, false /* defaultEnabled */); 146 } 147 148 /** 149 * Check whether or not one specific experimental feature for a particular namespace from 150 * {@link DeviceConfig} is enabled by comparing module package version 151 * with current version of property. If this property version is valid, the corresponding 152 * experimental feature would be enabled, otherwise disabled. 153 * 154 * This is useful to ensure that if a module install is rolled back, flags are not left fully 155 * rolled out on a version where they have not been well tested. 156 * @param context The global context information about an app environment. 157 * @param namespace The namespace containing the property to look up. 158 * @param name The name of the property to look up. 159 * @param defaultEnabled The value to return if the property does not exist or its value is 160 * null. 161 * @return true if this feature is enabled, or false if disabled. 162 */ isFeatureEnabled(@onNull Context context, @NonNull String namespace, @NonNull String name, boolean defaultEnabled)163 public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, 164 @NonNull String name, boolean defaultEnabled) { 165 try { 166 final long packageVersion = getPackageVersion(context); 167 return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); 168 } catch (PackageManager.NameNotFoundException e) { 169 Log.e(TAG, "Could not find the package name", e); 170 return false; 171 } 172 } 173 174 /** 175 * Check whether or not one specific experimental feature for a particular namespace from 176 * {@link DeviceConfig} is enabled by comparing module package version 177 * with current version of property. If this property version is valid, the corresponding 178 * experimental feature would be enabled, otherwise disabled. 179 * 180 * This is useful to ensure that if a module install is rolled back, flags are not left fully 181 * rolled out on a version where they have not been well tested. 182 * @param context The global context information about an app environment. 183 * @param namespace The namespace containing the property to look up. 184 * @param name The name of the property to look up. 185 * @param moduleName The mainline module name which is released as apex. 186 * @param defaultEnabled The value to return if the property does not exist or its value is 187 * null. 188 * @return true if this feature is enabled, or false if disabled. 189 */ isFeatureEnabled(@onNull Context context, @NonNull String namespace, @NonNull String name, @NonNull String moduleName, boolean defaultEnabled)190 public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, 191 @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) { 192 try { 193 final long packageVersion = getModuleVersion(context, moduleName); 194 return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); 195 } catch (PackageManager.NameNotFoundException e) { 196 Log.e(TAG, "Could not find the module name", e); 197 return false; 198 } 199 } 200 maybeUseFixedPackageVersion(@onNull Context context)201 private static boolean maybeUseFixedPackageVersion(@NonNull Context context) { 202 final String packageName = context.getPackageName(); 203 if (packageName == null) return false; 204 205 return packageName.equals("com.android.networkstack.tethering") 206 || packageName.equals("com.android.networkstack.tethering.inprocess"); 207 } 208 isFeatureEnabled(@onNull Context context, long packageVersion, @NonNull String namespace, String name, boolean defaultEnabled)209 private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion, 210 @NonNull String namespace, String name, boolean defaultEnabled) 211 throws PackageManager.NameNotFoundException { 212 final int propertyVersion = getDeviceConfigPropertyInt(namespace, name, 213 0 /* default value */); 214 return (propertyVersion == 0 && defaultEnabled) 215 || (propertyVersion != 0 && packageVersion >= (long) propertyVersion); 216 } 217 218 private static volatile long sModuleVersion = -1; 219 @VisibleForTesting public static long FIXED_PACKAGE_VERSION = 10; getModuleVersion(@onNull Context context, @NonNull String moduleName)220 private static long getModuleVersion(@NonNull Context context, @NonNull String moduleName) 221 throws PackageManager.NameNotFoundException { 222 if (sModuleVersion >= 0) return sModuleVersion; 223 224 final PackageManager packageManager = context.getPackageManager(); 225 ModuleInfo module; 226 try { 227 module = packageManager.getModuleInfo( 228 moduleName, PackageManager.MODULE_APEX_NAME); 229 } catch (PackageManager.NameNotFoundException e) { 230 // The error may happen if mainline module meta data is not installed e.g. there are 231 // no meta data configuration in AOSP build. To be able to enable a feature in AOSP 232 // by setting a flag via ADB for example. set a small non-zero fixed number for 233 // comparing. 234 if (maybeUseFixedPackageVersion(context)) { 235 sModuleVersion = FIXED_PACKAGE_VERSION; 236 return FIXED_PACKAGE_VERSION; 237 } else { 238 throw e; 239 } 240 } 241 String modulePackageName = module.getPackageName(); 242 if (modulePackageName == null) throw new PackageManager.NameNotFoundException(moduleName); 243 final long version = packageManager.getPackageInfo(modulePackageName, 244 PackageManager.MATCH_APEX).getLongVersionCode(); 245 sModuleVersion = version; 246 247 return version; 248 } 249 250 /** 251 * Gets boolean config from resources. 252 */ getResBooleanConfig(@onNull final Context context, @BoolRes int configResource, final boolean defaultValue)253 public static boolean getResBooleanConfig(@NonNull final Context context, 254 @BoolRes int configResource, final boolean defaultValue) { 255 final Resources res = context.getResources(); 256 try { 257 return res.getBoolean(configResource); 258 } catch (Resources.NotFoundException e) { 259 return defaultValue; 260 } 261 } 262 } 263