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