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