summaryrefslogtreecommitdiff
path: root/common/device/com/android/net/module/util/DeviceConfigUtils.java
blob: e646f37a6e2d5ad25e8bb9b1902a2c960a2918be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.net.module.util;

import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;

import static com.android.net.module.util.FeatureVersions.CONNECTIVITY_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.MODULE_MASK;
import static com.android.net.module.util.FeatureVersions.NETWORK_STACK_MODULE_ID;
import static com.android.net.module.util.FeatureVersions.VERSION_MASK;

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.provider.DeviceConfig;
import android.util.Log;

import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;

/**
 * Utilities for modules to query {@link DeviceConfig} and flags.
 */
public final class DeviceConfigUtils {
    private DeviceConfigUtils() {}

    private static final String TAG = DeviceConfigUtils.class.getSimpleName();
    /**
     * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value
     * until they are recompiled, so modifying this constant means that different modules may
     * be referencing a different tethering module variant, or having a stale reference.
     */
    public static final String TETHERING_MODULE_NAME = "com.android.tethering";

    @VisibleForTesting
    public static final String RESOURCES_APK_INTENT =
            "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
    private static final String CONNECTIVITY_RES_PKG_DIR = "/apex/" + TETHERING_MODULE_NAME + "/";

    @VisibleForTesting
    public static final long DEFAULT_PACKAGE_VERSION = 1000;

    @VisibleForTesting
    public static void resetPackageVersionCacheForTest() {
        sPackageVersion = -1;
        sModuleVersion = -1;
        sNetworkStackModuleVersion = -1;
    }

    private static volatile long sPackageVersion = -1;
    private static long getPackageVersion(@NonNull final Context context) {
        // sPackageVersion may be set by another thread just after this check, but querying the
        // package version several times on rare occasions is fine.
        if (sPackageVersion >= 0) {
            return sPackageVersion;
        }
        try {
            final long version = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0).getLongVersionCode();
            sPackageVersion = version;
            return version;
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Failed to get package info: " + e);
            return DEFAULT_PACKAGE_VERSION;
        }
    }

    /**
     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
     * @param namespace The namespace containing the property to look up.
     * @param name The name of the property to look up.
     * @param defaultValue The value to return if the property does not exist or has no valid value.
     * @return the corresponding value, or defaultValue if none exists.
     */
    @Nullable
    public static String getDeviceConfigProperty(@NonNull String namespace, @NonNull String name,
            @Nullable String defaultValue) {
        String value = DeviceConfig.getProperty(namespace, name);
        return value != null ? value : defaultValue;
    }

    /**
     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
     * @param namespace The namespace containing the property to look up.
     * @param name The name of the property to look up.
     * @param defaultValue The value to return if the property does not exist or its value is null.
     * @return the corresponding value, or defaultValue if none exists.
     */
    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
            int defaultValue) {
        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
        try {
            return (value != null) ? Integer.parseInt(value) : defaultValue;
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    /**
     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
     *
     * Flags like timeouts should use this method and set an appropriate min/max range: if invalid
     * values like "0" or "1" are pushed to devices, everything would timeout. The min/max range
     * protects against this kind of breakage.
     * @param namespace The namespace containing the property to look up.
     * @param name The name of the property to look up.
     * @param minimumValue The minimum value of a property.
     * @param maximumValue The maximum value of a property.
     * @param defaultValue The value to return if the property does not exist or its value is null.
     * @return the corresponding value, or defaultValue if none exists or the fetched value is
     *         not in the provided range.
     */
    public static int getDeviceConfigPropertyInt(@NonNull String namespace, @NonNull String name,
            int minimumValue, int maximumValue, int defaultValue) {
        int value = getDeviceConfigPropertyInt(namespace, name, defaultValue);
        if (value < minimumValue || value > maximumValue) return defaultValue;
        return value;
    }

    /**
     * Look up the value of a property for a particular namespace from {@link DeviceConfig}.
     * @param namespace The namespace containing the property to look up.
     * @param name The name of the property to look up.
     * @param defaultValue The value to return if the property does not exist or its value is null.
     * @return the corresponding value, or defaultValue if none exists.
     */
    public static boolean getDeviceConfigPropertyBoolean(@NonNull String namespace,
            @NonNull String name, boolean defaultValue) {
        String value = getDeviceConfigProperty(namespace, name, null /* defaultValue */);
        return (value != null) ? Boolean.parseBoolean(value) : defaultValue;
    }

    /**
     * Check whether or not one specific experimental feature for a particular namespace from
     * {@link DeviceConfig} is enabled by comparing module package version
     * with current version of property. If this property version is valid, the corresponding
     * experimental feature would be enabled, otherwise disabled.
     *
     * This is useful to ensure that if a module install is rolled back, flags are not left fully
     * rolled out on a version where they have not been well tested.
     * @param context The global context information about an app environment.
     * @param name The name of the property to look up.
     * @return true if this feature is enabled, or false if disabled.
     */
    public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
            @NonNull String name) {
        return isNetworkStackFeatureEnabled(context, name, false /* defaultEnabled */);
    }

    /**
     * Check whether or not one specific experimental feature for a particular namespace from
     * {@link DeviceConfig} is enabled by comparing module package version
     * with current version of property. If this property version is valid, the corresponding
     * experimental feature would be enabled, otherwise disabled.
     *
     * This is useful to ensure that if a module install is rolled back, flags are not left fully
     * rolled out on a version where they have not been well tested.
     * @param context The global context information about an app environment.
     * @param name The name of the property to look up.
     * @param defaultEnabled The value to return if the property does not exist or its value is
     *                       null.
     * @return true if this feature is enabled, or false if disabled.
     */
    public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
            @NonNull String name, boolean defaultEnabled) {
        final long packageVersion = getPackageVersion(context);
        return isFeatureEnabled(context, packageVersion, NAMESPACE_CONNECTIVITY, name,
                defaultEnabled);
    }

    /**
     * Check whether or not one specific experimental feature for a particular namespace from
     * {@link DeviceConfig} is enabled by comparing module package version
     * with current version of property. If this property version is valid, the corresponding
     * experimental feature would be enabled, otherwise disabled.
     *
     * This is useful to ensure that if a module install is rolled back, flags are not left fully
     * rolled out on a version where they have not been well tested.
     *
     * If the feature is disabled by default and enabled by flag push, this method should be used.
     * If the feature is enabled by default and disabled by flag push (kill switch),
     * {@link #isTetheringFeatureNotChickenedOut(String)} should be used.
     *
     * @param context The global context information about an app environment.
     * @param name The name of the property to look up.
     * @return true if this feature is enabled, or false if disabled.
     */
    public static boolean isTetheringFeatureEnabled(@NonNull Context context,
            @NonNull String name) {
        final long packageVersion = getTetheringModuleVersion(context);
        return isFeatureEnabled(context, packageVersion, NAMESPACE_TETHERING, name,
                false /* defaultEnabled */);
    }

    private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion,
            @NonNull String namespace, String name, boolean defaultEnabled) {
        final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
                0 /* default value */);
        return (propertyVersion == 0 && defaultEnabled)
                || (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
    }

    // Guess the tethering module name based on the package prefix of the connectivity resources
    // Take the resource package name, cut it before "connectivity" and append "tethering".
    // Then resolve that package version number with packageManager.
    // If that fails retry by appending "go.tethering" instead
    private static long resolveTetheringModuleVersion(@NonNull Context context)
            throws PackageManager.NameNotFoundException {
        final String pkgPrefix = resolvePkgPrefix(context);
        final PackageManager packageManager = context.getPackageManager();
        try {
            return packageManager.getPackageInfo(pkgPrefix + "tethering",
                    PackageManager.MATCH_APEX).getLongVersionCode();
        } catch (PackageManager.NameNotFoundException e) {
            Log.d(TAG, "Device is using go modules");
            // fall through
        }

        return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
                PackageManager.MATCH_APEX).getLongVersionCode();
    }

    private static String resolvePkgPrefix(Context context) {
        final String connResourcesPackage = getConnectivityResourcesPackageName(context);
        final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity");
        if (pkgPrefixLen < 0) {
            throw new IllegalStateException(
                    "Invalid connectivity resources package: " + connResourcesPackage);
        }

        return connResourcesPackage.substring(0, pkgPrefixLen);
    }

    private static volatile long sModuleVersion = -1;
    private static long getTetheringModuleVersion(@NonNull Context context) {
        if (sModuleVersion >= 0) return sModuleVersion;

        try {
            sModuleVersion = resolveTetheringModuleVersion(context);
        } catch (PackageManager.NameNotFoundException e) {
            // It's expected to fail tethering module version resolution on the devices with
            // flattened apex
            Log.e(TAG, "Failed to resolve tethering module version: " + e);
            return DEFAULT_PACKAGE_VERSION;
        }
        return sModuleVersion;
    }

    private static volatile long sNetworkStackModuleVersion = -1;

    /**
     * Get networkstack module version.
     */
    @VisibleForTesting
    static long getNetworkStackModuleVersion(@NonNull Context context) {
        if (sNetworkStackModuleVersion >= 0) return sNetworkStackModuleVersion;

        try {
            sNetworkStackModuleVersion = resolveNetworkStackModuleVersion(context);
        } catch (PackageManager.NameNotFoundException e) {
            Log.wtf(TAG, "Failed to resolve networkstack module version: " + e);
            return DEFAULT_PACKAGE_VERSION;
        }
        return sNetworkStackModuleVersion;
    }

    private static long resolveNetworkStackModuleVersion(@NonNull Context context)
            throws PackageManager.NameNotFoundException {
        // TODO(b/293975546): Strictly speaking this is the prefix for connectivity and not
        //  network stack. In practice, it's the same. Read the prefix from network stack instead.
        final String pkgPrefix = resolvePkgPrefix(context);
        final PackageManager packageManager = context.getPackageManager();
        try {
            return packageManager.getPackageInfo(pkgPrefix + "networkstack",
                    PackageManager.MATCH_SYSTEM_ONLY).getLongVersionCode();
        } catch (PackageManager.NameNotFoundException e) {
            Log.d(TAG, "Device is using go or non-mainline modules");
            // fall through
        }

        return packageManager.getPackageInfo(pkgPrefix + "go.networkstack",
                PackageManager.MATCH_ALL).getLongVersionCode();
    }

    /**
     * Check whether one specific feature is supported from the feature Id. The feature Id is
     * composed by a module package Id and version Id from {@link FeatureVersions}.
     *
     * This is useful when a feature required minimal module version supported and cannot function
     * well with a standalone newer module.
     * @param context The global context information about an app environment.
     * @param featureId The feature id that contains required module id and minimal module version
     * @return true if this feature is supported, or false if not supported.
     **/
    public static boolean isFeatureSupported(@NonNull Context context, long featureId) {
        final long moduleVersion;
        final long moduleId = featureId & MODULE_MASK;
        if (moduleId == CONNECTIVITY_MODULE_ID) {
            moduleVersion = getTetheringModuleVersion(context);
        } else if (moduleId == NETWORK_STACK_MODULE_ID) {
            moduleVersion = getNetworkStackModuleVersion(context);
        } else {
            throw new IllegalArgumentException("Unknown module " + moduleId);
        }
        // Support by default if no module version is available.
        return moduleVersion == DEFAULT_PACKAGE_VERSION
                || moduleVersion >= (featureId & VERSION_MASK);
    }

    /**
     * Check whether one specific experimental feature in specific namespace from
     * {@link DeviceConfig} is not disabled. Feature can be disabled by setting a non-zero
     * value in the property. If the feature is enabled by default and disabled by flag push
     * (kill switch), this method should be used. If the feature is disabled by default and
     * enabled by flag push, {@link #isFeatureEnabled} should be used.
     *
     * @param namespace The namespace containing the property to look up.
     * @param name The name of the property to look up.
     * @return true if this feature is enabled, or false if disabled.
     */
    private static boolean isFeatureNotChickenedOut(String namespace, String name) {
        final int propertyVersion = getDeviceConfigPropertyInt(namespace, name,
                0 /* default value */);
        return propertyVersion == 0;
    }

    /**
     * Check whether one specific experimental feature in Tethering module from {@link DeviceConfig}
     * is not disabled.
     *
     * @param name The name of the property in tethering module to look up.
     * @return true if this feature is enabled, or false if disabled.
     */
    public static boolean isTetheringFeatureNotChickenedOut(String name) {
        return isFeatureNotChickenedOut(NAMESPACE_TETHERING, name);
    }

    /**
     * Check whether one specific experimental feature in NetworkStack module from
     * {@link DeviceConfig} is not disabled.
     *
     * @param name The name of the property in NetworkStack module to look up.
     * @return true if this feature is enabled, or false if disabled.
     */
    public static boolean isNetworkStackFeatureNotChickenedOut(String name) {
        return isFeatureNotChickenedOut(NAMESPACE_CONNECTIVITY, name);
    }

    /**
     * Gets boolean config from resources.
     */
    public static boolean getResBooleanConfig(@NonNull final Context context,
            @BoolRes int configResource, final boolean defaultValue) {
        final Resources res = context.getResources();
        try {
            return res.getBoolean(configResource);
        } catch (Resources.NotFoundException e) {
            return defaultValue;
        }
    }

    /**
     * Gets int config from resources.
     */
    public static int getResIntegerConfig(@NonNull final Context context,
            @BoolRes int configResource, final int defaultValue) {
        final Resources res = context.getResources();
        try {
            return res.getInteger(configResource);
        } catch (Resources.NotFoundException e) {
            return defaultValue;
        }
    }

    /**
     * Get the package name of the ServiceConnectivityResources package, used to provide resources
     * for service-connectivity.
     */
    @NonNull
    public static String getConnectivityResourcesPackageName(@NonNull Context context) {
        final List<ResolveInfo> pkgs = new ArrayList<>(context.getPackageManager()
                .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY));
        pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(
                CONNECTIVITY_RES_PKG_DIR));
        if (pkgs.size() > 1) {
            Log.wtf(TAG, "More than one connectivity resources package found: " + pkgs);
        }
        if (pkgs.isEmpty()) {
            throw new IllegalStateException("No connectivity resource package found");
        }

        return pkgs.get(0).activityInfo.applicationInfo.packageName;
    }
}