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