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 @VisibleForTesting resetPackageVersionCacheForTest()40 public static void resetPackageVersionCacheForTest() { 41 sPackageVersion = -1; 42 sModuleVersion = -1; 43 } 44 45 private static volatile long sPackageVersion = -1; getPackageVersion(@onNull final Context context)46 private static long getPackageVersion(@NonNull final Context context) 47 throws PackageManager.NameNotFoundException { 48 // sPackageVersion may be set by another thread just after this check, but querying the 49 // package version several times on rare occasions is fine. 50 if (sPackageVersion >= 0) { 51 return sPackageVersion; 52 } 53 final long version = context.getPackageManager().getPackageInfo( 54 context.getPackageName(), 0).getLongVersionCode(); 55 sPackageVersion = version; 56 return version; 57 } 58 59 /** 60 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 61 * @param namespace The namespace containing the property to look up. 62 * @param name The name of the property to look up. 63 * @param defaultValue The value to return if the property does not exist or has no valid value. 64 * @return the corresponding value, or defaultValue if none exists. 65 */ 66 @Nullable getDeviceConfigProperty(@onNull String namespace, @NonNull String name, @Nullable String defaultValue)67 public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name, 68 @Nullable String defaultValue) { 69 String value = DeviceConfig.getProperty(namespace, name); 70 return value != null ? value : defaultValue; 71 } 72 73 /** 74 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 75 * @param namespace The namespace containing the property to look up. 76 * @param name The name of the property to look up. 77 * @param defaultValue The value to return if the property does not exist or its value is null. 78 * @return the corresponding value, or defaultValue if none exists. 79 */ getDeviceConfigPropertyInt(@onNull String namespace, @NonNull String name, int defaultValue)80 public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, 81 int defaultValue) { 82 String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); 83 try { 84 return (value != null) ? Integer.parseInt(value) : defaultValue; 85 } catch (NumberFormatException e) { 86 return defaultValue; 87 } 88 } 89 90 /** 91 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 92 * 93 * Flags like timeouts should use this method and set an appropriate min/max range: if invalid 94 * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range 95 * protects against this kind of breakage. 96 * @param namespace The namespace containing the property to look up. 97 * @param name The name of the property to look up. 98 * @param minimumValue The minimum value of a property. 99 * @param maximumValue The maximum value of a property. 100 * @param defaultValue The value to return if the property does not exist or its value is null. 101 * @return the corresponding value, or defaultValue if none exists or the fetched value is 102 * not in the provided range. 103 */ getDeviceConfigPropertyInt(@onNull String namespace, @NonNull String name, int minimumValue, int maximumValue, int defaultValue)104 public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, 105 int minimumValue, int maximumValue, int defaultValue) { 106 int value = getDeviceConfigPropertyInt(namespace, name, defaultValue); 107 if (value < minimumValue || value > maximumValue) return defaultValue; 108 return value; 109 } 110 111 /** 112 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 113 * @param namespace The namespace containing the property to look up. 114 * @param name The name of the property to look up. 115 * @param defaultValue The value to return if the property does not exist or its value is null. 116 * @return the corresponding value, or defaultValue if none exists. 117 */ getDeviceConfigPropertyBoolean(@onNull String namespace, @NonNull String name, boolean defaultValue)118 public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace, 119 @NonNull String name, boolean defaultValue) { 120 String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); 121 return (value != null) ? Boolean.parseBoolean(value) : defaultValue; 122 } 123 124 /** 125 * Check whether or not one specific experimental feature for a particular namespace from 126 * {@link DeviceConfig} is enabled by comparing module package version 127 * with current version of property. If this property version is valid, the corresponding 128 * experimental feature would be enabled, otherwise disabled. 129 * 130 * This is useful to ensure that if a module install is rolled back, flags are not left fully 131 * rolled out on a version where they have not been well tested. 132 * @param context The global context information about an app environment. 133 * @param namespace The namespace containing the property to look up. 134 * @param name The name of the property to look up. 135 * @return true if this feature is enabled, or false if disabled. 136 */ isFeatureEnabled(@onNull Context context, @NonNull String namespace, @NonNull String name)137 public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, 138 @NonNull String name) { 139 return isFeatureEnabled(context, namespace, name, false /* defaultEnabled */); 140 } 141 142 /** 143 * Check whether or not one specific experimental feature for a particular namespace from 144 * {@link DeviceConfig} is enabled by comparing module package version 145 * with current version of property. If this property version is valid, the corresponding 146 * experimental feature would be enabled, otherwise disabled. 147 * 148 * This is useful to ensure that if a module install is rolled back, flags are not left fully 149 * rolled out on a version where they have not been well tested. 150 * @param context The global context information about an app environment. 151 * @param namespace The namespace containing the property to look up. 152 * @param name The name of the property to look up. 153 * @param defaultEnabled The value to return if the property does not exist or its value is 154 * null. 155 * @return true if this feature is enabled, or false if disabled. 156 */ isFeatureEnabled(@onNull Context context, @NonNull String namespace, @NonNull String name, boolean defaultEnabled)157 public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, 158 @NonNull String name, boolean defaultEnabled) { 159 try { 160 final long packageVersion = getPackageVersion(context); 161 return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); 162 } catch (PackageManager.NameNotFoundException e) { 163 Log.e(TAG, "Could not find the package name", e); 164 return false; 165 } 166 } 167 168 /** 169 * Check whether or not one specific experimental feature for a particular namespace from 170 * {@link DeviceConfig} is enabled by comparing module package version 171 * with current version of property. If this property version is valid, the corresponding 172 * experimental feature would be enabled, otherwise disabled. 173 * 174 * This is useful to ensure that if a module install is rolled back, flags are not left fully 175 * rolled out on a version where they have not been well tested. 176 * @param context The global context information about an app environment. 177 * @param namespace The namespace containing the property to look up. 178 * @param name The name of the property to look up. 179 * @param moduleName The mainline module name which is released as apex. 180 * @param defaultEnabled The value to return if the property does not exist or its value is 181 * null. 182 * @return true if this feature is enabled, or false if disabled. 183 */ isFeatureEnabled(@onNull Context context, @NonNull String namespace, @NonNull String name, @NonNull String moduleName, boolean defaultEnabled)184 public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace, 185 @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) { 186 try { 187 final long packageVersion = getModuleVersion(context, moduleName); 188 return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled); 189 } catch (PackageManager.NameNotFoundException e) { 190 Log.e(TAG, "Could not find the module name", e); 191 return false; 192 } 193 } 194 maybeUseFixedPackageVersion(@onNull Context context)195 private static boolean maybeUseFixedPackageVersion(@NonNull Context context) { 196 final String packageName = context.getPackageName(); 197 if (packageName == null) return false; 198 199 return packageName.equals("com.android.networkstack.tethering") 200 || packageName.equals("com.android.networkstack.tethering.inprocess"); 201 } 202 isFeatureEnabled(@onNull Context context, long packageVersion, @NonNull String namespace, String name, boolean defaultEnabled)203 private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion, 204 @NonNull String namespace, String name, boolean defaultEnabled) 205 throws PackageManager.NameNotFoundException { 206 final int propertyVersion = getDeviceConfigPropertyInt(namespace, name, 207 0 /* default value */); 208 return (propertyVersion == 0 && defaultEnabled) 209 || (propertyVersion != 0 && packageVersion >= (long) propertyVersion); 210 } 211 212 private static volatile long sModuleVersion = -1; 213 @VisibleForTesting public static long FIXED_PACKAGE_VERSION = 10; getModuleVersion(@onNull Context context, @NonNull String moduleName)214 private static long getModuleVersion(@NonNull Context context, @NonNull String moduleName) 215 throws PackageManager.NameNotFoundException { 216 if (sModuleVersion >= 0) return sModuleVersion; 217 218 final PackageManager packageManager = context.getPackageManager(); 219 ModuleInfo module; 220 try { 221 module = packageManager.getModuleInfo( 222 moduleName, PackageManager.MODULE_APEX_NAME); 223 } catch (PackageManager.NameNotFoundException e) { 224 // The error may happen if mainline module meta data is not installed e.g. there are 225 // no meta data configuration in AOSP build. To be able to enable a feature in AOSP 226 // by setting a flag via ADB for example. set a small non-zero fixed number for 227 // comparing. 228 if (maybeUseFixedPackageVersion(context)) { 229 sModuleVersion = FIXED_PACKAGE_VERSION; 230 return FIXED_PACKAGE_VERSION; 231 } else { 232 throw e; 233 } 234 } 235 String modulePackageName = module.getPackageName(); 236 if (modulePackageName == null) throw new PackageManager.NameNotFoundException(moduleName); 237 final long version = packageManager.getPackageInfo(modulePackageName, 238 PackageManager.MATCH_APEX).getLongVersionCode(); 239 sModuleVersion = version; 240 241 return version; 242 } 243 244 /** 245 * Gets boolean config from resources. 246 */ getResBooleanConfig(@onNull final Context context, @BoolRes int configResource, final boolean defaultValue)247 public static boolean getResBooleanConfig(@NonNull final Context context, 248 @BoolRes int configResource, final boolean defaultValue) { 249 final Resources res = context.getResources(); 250 try { 251 return res.getBoolean(configResource); 252 } catch (Resources.NotFoundException e) { 253 return defaultValue; 254 } 255 } 256 } 257