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 static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; 20 import static android.provider.DeviceConfig.NAMESPACE_CAPTIVEPORTALLOGIN; 21 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; 22 import static android.provider.DeviceConfig.NAMESPACE_TETHERING; 23 24 import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID; 25 import static com.android.net.module.util.FeatureVersions.DNS_RESOLVER_MODULE_ID; 26 import static com.android.net.module.util.FeatureVersions.MODULE_MASK; 27 import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID; 28 import static com.android.net.module.util.FeatureVersions.VERSION_MASK; 29 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ResolveInfo; 34 import android.content.res.Resources; 35 import android.provider.DeviceConfig; 36 import android.util.Log; 37 38 import androidx.annotation.BoolRes; 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.VisibleForTesting; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.function.Supplier; 46 47 /** 48 * Utilities for modules to query {@link DeviceConfig} and flags. 49 */ 50 public final class DeviceConfigUtils { DeviceConfigUtils()51 private DeviceConfigUtils() {} 52 53 private static final String TAG = DeviceConfigUtils.class.getSimpleName(); 54 /** 55 * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value 56 * until they are recompiled, so modifying this constant means that different modules may 57 * be referencing a different tethering module variant, or having a stale reference. 58 */ 59 public static final String TETHERING_MODULE_NAME = "com.android.tethering"; 60 61 @VisibleForTesting 62 public static final String RESOURCES_APK_INTENT = 63 "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK"; 64 private static final String CONNECTIVITY_RES_PKG_DIR = "/apex/" + TETHERING_MODULE_NAME + "/"; 65 66 @VisibleForTesting 67 public static final long DEFAULT_PACKAGE_VERSION = 1000; 68 69 @VisibleForTesting resetPackageVersionCacheForTest()70 public static void resetPackageVersionCacheForTest() { 71 sPackageVersion = -1; 72 sTetheringModuleVersion = -1; 73 sResolvModuleVersion = -1; 74 sNetworkStackModuleVersion = -1; 75 } 76 77 private static final int FORCE_ENABLE_FEATURE_FLAG_VALUE = 1; 78 private static final int FORCE_DISABLE_FEATURE_FLAG_VALUE = -1; 79 80 private static volatile long sPackageVersion = -1; getPackageVersion(@onNull final Context context)81 private static long getPackageVersion(@NonNull final Context context) { 82 // sPackageVersion may be set by another thread just after this check, but querying the 83 // package version several times on rare occasions is fine. 84 if (sPackageVersion >= 0) { 85 return sPackageVersion; 86 } 87 try { 88 final long version = context.getPackageManager().getPackageInfo( 89 context.getPackageName(), 0).getLongVersionCode(); 90 sPackageVersion = version; 91 return version; 92 } catch (PackageManager.NameNotFoundException e) { 93 Log.e(TAG, "Failed to get package info: " + e); 94 return DEFAULT_PACKAGE_VERSION; 95 } 96 } 97 98 /** 99 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 100 * @param namespace The namespace containing the property to look up. 101 * @param name The name of the property to look up. 102 * @param defaultValue The value to return if the property does not exist or has no valid value. 103 * @return the corresponding value, or defaultValue if none exists. 104 */ 105 @Nullable getDeviceConfigProperty(@onNull String namespace, @NonNull String name, @Nullable String defaultValue)106 public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name, 107 @Nullable String defaultValue) { 108 String value = DeviceConfig.getProperty(namespace, name); 109 return value != null ? value : defaultValue; 110 } 111 112 /** 113 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 114 * @param namespace The namespace containing the property to look up. 115 * @param name The name of the property to look up. 116 * @param defaultValue The value to return if the property does not exist or its value is null. 117 * @return the corresponding value, or defaultValue if none exists. 118 */ getDeviceConfigPropertyInt(@onNull String namespace, @NonNull String name, int defaultValue)119 public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, 120 int defaultValue) { 121 String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); 122 try { 123 return (value != null) ? Integer.parseInt(value) : defaultValue; 124 } catch (NumberFormatException e) { 125 return defaultValue; 126 } 127 } 128 129 /** 130 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 131 * 132 * Flags like timeouts should use this method and set an appropriate min/max range: if invalid 133 * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range 134 * protects against this kind of breakage. 135 * @param namespace The namespace containing the property to look up. 136 * @param name The name of the property to look up. 137 * @param minimumValue The minimum value of a property. 138 * @param maximumValue The maximum value of a property. 139 * @param defaultValue The value to return if the property does not exist or its value is null. 140 * @return the corresponding value, or defaultValue if none exists or the fetched value is 141 * not in the provided range. 142 */ getDeviceConfigPropertyInt(@onNull String namespace, @NonNull String name, int minimumValue, int maximumValue, int defaultValue)143 public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name, 144 int minimumValue, int maximumValue, int defaultValue) { 145 int value = getDeviceConfigPropertyInt(namespace, name, defaultValue); 146 if (value < minimumValue || value > maximumValue) return defaultValue; 147 return value; 148 } 149 150 /** 151 * Look up the value of a property for a particular namespace from {@link DeviceConfig}. 152 * @param namespace The namespace containing the property to look up. 153 * @param name The name of the property to look up. 154 * @param defaultValue The value to return if the property does not exist or its value is null. 155 * @return the corresponding value, or defaultValue if none exists. 156 */ getDeviceConfigPropertyBoolean(@onNull String namespace, @NonNull String name, boolean defaultValue)157 public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace, 158 @NonNull String name, boolean defaultValue) { 159 String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */); 160 return (value != null) ? Boolean.parseBoolean(value) : defaultValue; 161 } 162 163 /** 164 * Check whether or not one specific experimental feature for a particular namespace from 165 * {@link DeviceConfig} is enabled by comparing module package version 166 * with current version of property. If this property version is valid, the corresponding 167 * experimental feature would be enabled, otherwise disabled. 168 * 169 * This is useful to ensure that if a module install is rolled back, flags are not left fully 170 * rolled out on a version where they have not been well tested. 171 * 172 * If the feature is disabled by default and enabled by flag push, this method should be used. 173 * If the feature is enabled by default and disabled by flag push (kill switch), 174 * {@link #isNetworkStackFeatureNotChickenedOut(Context, String)} should be used. 175 * 176 * @param context The global context information about an app environment. 177 * @param name The name of the property to look up. 178 * @return true if this feature is enabled, or false if disabled. 179 */ isNetworkStackFeatureEnabled(@onNull Context context, @NonNull String name)180 public static boolean isNetworkStackFeatureEnabled(@NonNull Context context, 181 @NonNull String name) { 182 return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, false /* defaultEnabled */, 183 () -> getPackageVersion(context)); 184 } 185 186 /** 187 * Check whether or not one specific experimental feature for a particular namespace from 188 * {@link DeviceConfig} is enabled by comparing module package version 189 * with current version of property. If this property version is valid, the corresponding 190 * experimental feature would be enabled, otherwise disabled. 191 * 192 * This is useful to ensure that if a module install is rolled back, flags are not left fully 193 * rolled out on a version where they have not been well tested. 194 * 195 * If the feature is disabled by default and enabled by flag push, this method should be used. 196 * If the feature is enabled by default and disabled by flag push (kill switch), 197 * {@link #isTetheringFeatureNotChickenedOut(Context, String)} should be used. 198 * 199 * @param context The global context information about an app environment. 200 * @param name The name of the property to look up. 201 * @return true if this feature is enabled, or false if disabled. 202 */ isTetheringFeatureEnabled(@onNull Context context, @NonNull String name)203 public static boolean isTetheringFeatureEnabled(@NonNull Context context, 204 @NonNull String name) { 205 return isFeatureEnabled(NAMESPACE_TETHERING, name, false /* defaultEnabled */, 206 () -> getTetheringModuleVersion(context)); 207 } 208 209 /** 210 * Check whether or not one specific experimental feature for a particular namespace from 211 * {@link DeviceConfig} is enabled by comparing module package version 212 * with current version of property. If this property version is valid, the corresponding 213 * experimental feature would be enabled, otherwise disabled. 214 * 215 * This is useful to ensure that if a module install is rolled back, flags are not left fully 216 * rolled out on a version where they have not been well tested. 217 * 218 * If the feature is disabled by default and enabled by flag push, this method should be used. 219 * If the feature is enabled by default and disabled by flag push (kill switch), 220 * {@link #isCaptivePortalLoginFeatureNotChickenedOut(Context, String)} should be used. 221 * 222 * @param context The global context information about an app environment. 223 * @param name The name of the property to look up. 224 * @return true if this feature is enabled, or false if disabled. 225 */ isCaptivePortalLoginFeatureEnabled(@onNull Context context, @NonNull String name)226 public static boolean isCaptivePortalLoginFeatureEnabled(@NonNull Context context, 227 @NonNull String name) { 228 return isFeatureEnabled(NAMESPACE_CAPTIVEPORTALLOGIN, name, false /* defaultEnabled */, 229 () -> getPackageVersion(context)); 230 } 231 isFeatureEnabled(@onNull String namespace, String name, boolean defaultEnabled, Supplier<Long> packageVersionSupplier)232 private static boolean isFeatureEnabled(@NonNull String namespace, 233 String name, boolean defaultEnabled, Supplier<Long> packageVersionSupplier) { 234 final int flagValue = getDeviceConfigPropertyInt(namespace, name, 0 /* default value */); 235 switch (flagValue) { 236 case 0: 237 return defaultEnabled; 238 case FORCE_DISABLE_FEATURE_FLAG_VALUE: 239 return false; 240 case FORCE_ENABLE_FEATURE_FLAG_VALUE: 241 return true; 242 default: 243 final long packageVersion = packageVersionSupplier.get(); 244 return packageVersion >= (long) flagValue; 245 } 246 } 247 248 // Guess an APEX module name based on the package prefix of the connectivity resources 249 // Take the resource package name, cut it before "connectivity" and append the module name. 250 // Then resolve that package version number with packageManager. 251 // If that fails retry by appending "go.<moduleName>" instead. resolveApexModuleVersion(@onNull Context context, String moduleName)252 private static long resolveApexModuleVersion(@NonNull Context context, String moduleName) 253 throws PackageManager.NameNotFoundException { 254 final String pkgPrefix = resolvePkgPrefix(context); 255 final PackageManager packageManager = context.getPackageManager(); 256 try { 257 return packageManager.getPackageInfo(pkgPrefix + moduleName, 258 PackageManager.MATCH_APEX).getLongVersionCode(); 259 } catch (PackageManager.NameNotFoundException e) { 260 Log.d(TAG, "Device is using go modules"); 261 // fall through 262 } 263 264 return packageManager.getPackageInfo(pkgPrefix + "go." + moduleName, 265 PackageManager.MATCH_APEX).getLongVersionCode(); 266 } 267 resolvePkgPrefix(Context context)268 private static String resolvePkgPrefix(Context context) { 269 final String connResourcesPackage = getConnectivityResourcesPackageName(context); 270 final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity"); 271 if (pkgPrefixLen < 0) { 272 throw new IllegalStateException( 273 "Invalid connectivity resources package: " + connResourcesPackage); 274 } 275 276 return connResourcesPackage.substring(0, pkgPrefixLen); 277 } 278 279 private static volatile long sTetheringModuleVersion = -1; 280 getTetheringModuleVersion(@onNull Context context)281 private static long getTetheringModuleVersion(@NonNull Context context) { 282 if (sTetheringModuleVersion >= 0) return sTetheringModuleVersion; 283 284 try { 285 sTetheringModuleVersion = resolveApexModuleVersion(context, "tethering"); 286 } catch (PackageManager.NameNotFoundException e) { 287 // It's expected to fail tethering module version resolution on the devices with 288 // flattened apex 289 Log.e(TAG, "Failed to resolve tethering module version: " + e); 290 return DEFAULT_PACKAGE_VERSION; 291 } 292 return sTetheringModuleVersion; 293 } 294 295 private static volatile long sResolvModuleVersion = -1; getResolvModuleVersion(@onNull Context context)296 private static long getResolvModuleVersion(@NonNull Context context) { 297 if (sResolvModuleVersion >= 0) return sResolvModuleVersion; 298 299 try { 300 sResolvModuleVersion = resolveApexModuleVersion(context, "resolv"); 301 } catch (PackageManager.NameNotFoundException e) { 302 // It's expected to fail resolv module version resolution on the devices with 303 // flattened apex 304 Log.e(TAG, "Failed to resolve resolv module version: " + e); 305 return DEFAULT_PACKAGE_VERSION; 306 } 307 return sResolvModuleVersion; 308 } 309 310 private static volatile long sNetworkStackModuleVersion = -1; 311 312 /** 313 * Get networkstack module version. 314 */ 315 @VisibleForTesting getNetworkStackModuleVersion(@onNull Context context)316 static long getNetworkStackModuleVersion(@NonNull Context context) { 317 if (sNetworkStackModuleVersion >= 0) return sNetworkStackModuleVersion; 318 319 try { 320 sNetworkStackModuleVersion = resolveNetworkStackModuleVersion(context); 321 } catch (PackageManager.NameNotFoundException e) { 322 Log.wtf(TAG, "Failed to resolve networkstack module version: " + e); 323 return DEFAULT_PACKAGE_VERSION; 324 } 325 return sNetworkStackModuleVersion; 326 } 327 resolveNetworkStackModuleVersion(@onNull Context context)328 private static long resolveNetworkStackModuleVersion(@NonNull Context context) 329 throws PackageManager.NameNotFoundException { 330 // TODO(b/293975546): Strictly speaking this is the prefix for connectivity and not 331 // network stack. In practice, it's the same. Read the prefix from network stack instead. 332 final String pkgPrefix = resolvePkgPrefix(context); 333 final PackageManager packageManager = context.getPackageManager(); 334 try { 335 return packageManager.getPackageInfo(pkgPrefix + "networkstack", 336 PackageManager.MATCH_SYSTEM_ONLY).getLongVersionCode(); 337 } catch (PackageManager.NameNotFoundException e) { 338 Log.d(TAG, "Device is using go or non-mainline modules"); 339 // fall through 340 } 341 342 return packageManager.getPackageInfo(pkgPrefix + "go.networkstack", 343 PackageManager.MATCH_ALL).getLongVersionCode(); 344 } 345 346 /** 347 * Check whether one specific feature is supported from the feature Id. The feature Id is 348 * composed by a module package Id and version Id from {@link FeatureVersions}. 349 * 350 * This is useful when a feature required minimal module version supported and cannot function 351 * well with a standalone newer module. 352 * @param context The global context information about an app environment. 353 * @param featureId The feature id that contains required module id and minimal module version 354 * @return true if this feature is supported, or false if not supported. 355 **/ isFeatureSupported(@onNull Context context, long featureId)356 public static boolean isFeatureSupported(@NonNull Context context, long featureId) { 357 final long moduleVersion; 358 final long moduleId = featureId & MODULE_MASK; 359 if (moduleId == CONNECTIVITY_MODULE_ID) { 360 moduleVersion = getTetheringModuleVersion(context); 361 } else if (moduleId == NETWORK_STACK_MODULE_ID) { 362 moduleVersion = getNetworkStackModuleVersion(context); 363 } else if (moduleId == DNS_RESOLVER_MODULE_ID) { 364 moduleVersion = getResolvModuleVersion(context); 365 } else { 366 throw new IllegalArgumentException("Unknown module " + moduleId); 367 } 368 // Support by default if no module version is available. 369 return moduleVersion == DEFAULT_PACKAGE_VERSION 370 || moduleVersion >= (featureId & VERSION_MASK); 371 } 372 373 /** 374 * Check whether one specific experimental feature in Tethering module from {@link DeviceConfig} 375 * is not disabled. 376 * If the feature is enabled by default and disabled by flag push (kill switch), this method 377 * should be used. 378 * If the feature is disabled by default and enabled by flag push, 379 * {@link #isTetheringFeatureEnabled(Context, String)} should be used. 380 * 381 * @param context The global context information about an app environment. 382 * @param name The name of the property in tethering module to look up. 383 * @return true if this feature is enabled, or false if disabled. 384 */ isTetheringFeatureNotChickenedOut(@onNull Context context, String name)385 public static boolean isTetheringFeatureNotChickenedOut(@NonNull Context context, String name) { 386 return isFeatureEnabled(NAMESPACE_TETHERING, name, true /* defaultEnabled */, 387 () -> getTetheringModuleVersion(context)); 388 } 389 390 /** 391 * Check whether one specific experimental feature in NetworkStack module from 392 * {@link DeviceConfig} is not disabled. 393 * If the feature is enabled by default and disabled by flag push (kill switch), this method 394 * should be used. 395 * If the feature is disabled by default and enabled by flag push, 396 * {@link #isNetworkStackFeatureEnabled(Context, String)} should be used. 397 * 398 * @param context The global context information about an app environment. 399 * @param name The name of the property in NetworkStack module to look up. 400 * @return true if this feature is enabled, or false if disabled. 401 */ isNetworkStackFeatureNotChickenedOut( @onNull Context context, String name)402 public static boolean isNetworkStackFeatureNotChickenedOut( 403 @NonNull Context context, String name) { 404 return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, true /* defaultEnabled */, 405 () -> getPackageVersion(context)); 406 } 407 408 /** 409 * Gets boolean config from resources. 410 */ getResBooleanConfig(@onNull final Context context, @BoolRes int configResource, final boolean defaultValue)411 public static boolean getResBooleanConfig(@NonNull final Context context, 412 @BoolRes int configResource, final boolean defaultValue) { 413 final Resources res = context.getResources(); 414 try { 415 return res.getBoolean(configResource); 416 } catch (Resources.NotFoundException e) { 417 return defaultValue; 418 } 419 } 420 421 /** 422 * Gets int config from resources. 423 */ getResIntegerConfig(@onNull final Context context, @BoolRes int configResource, final int defaultValue)424 public static int getResIntegerConfig(@NonNull final Context context, 425 @BoolRes int configResource, final int defaultValue) { 426 final Resources res = context.getResources(); 427 try { 428 return res.getInteger(configResource); 429 } catch (Resources.NotFoundException e) { 430 return defaultValue; 431 } 432 } 433 434 /** 435 * Get the package name of the ServiceConnectivityResources package, used to provide resources 436 * for service-connectivity. 437 */ 438 @NonNull getConnectivityResourcesPackageName(@onNull Context context)439 public static String getConnectivityResourcesPackageName(@NonNull Context context) { 440 final List<ResolveInfo> pkgs = new ArrayList<>(context.getPackageManager() 441 .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY)); 442 pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith( 443 CONNECTIVITY_RES_PKG_DIR)); 444 if (pkgs.size() > 1) { 445 Log.wtf(TAG, "More than one connectivity resources package found: " + pkgs); 446 } 447 if (pkgs.isEmpty()) { 448 throw new IllegalStateException("No connectivity resource package found"); 449 } 450 451 return pkgs.get(0).activityInfo.applicationInfo.packageName; 452 } 453 } 454