• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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