1 /*
2  * Copyright (C) 2017 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.compatibility.common.util;
18 
19 import android.os.Build;
20 
21 import androidx.test.InstrumentationRegistry;
22 
23 import java.io.IOException;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.Scanner;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 
30 /**
31  * Device-side utility class for reading properties and gathering information for testing
32  * Android device compatibility.
33  */
34 public class PropertyUtil {
35 
36     /**
37      * Name of read-only property detailing the first API level for which the product was
38      * shipped. Property should be undefined for factory ROM products.
39      */
40     public static final String FIRST_API_LEVEL = "ro.product.first_api_level";
41     private static final String BOARD_API_LEVEL = "ro.board.api_level";
42     private static final String BOARD_FIRST_API_LEVEL = "ro.board.first_api_level";
43     private static final String BUILD_TYPE_PROPERTY = "ro.build.type";
44     private static final String CAMERAX_EXTENSIONS_ENABLED = "ro.camerax.extensions.enabled";
45     private static final String MANUFACTURER_PROPERTY = "ro.product.manufacturer";
46     private static final String TAG_DEV_KEYS = "dev-keys";
47     private static final String TAG_TEST_KEYS = "test-keys";
48     private static final String VENDOR_API_LEVEL = "ro.vendor.api_level";
49     private static final String VNDK_VERSION = "ro.vndk.version";
50 
51     public static final String GOOGLE_SETTINGS_QUERY =
52             "content query --uri content://com.google.settings/partner";
53 
54     /** Value to be returned by getPropertyInt() if property is not found */
55     public static int INT_VALUE_IF_UNSET = -1;
56 
57     /** API level for current in development */
58     public static final int API_LEVEL_CURRENT = 10000;
59 
60     /** Returns whether the device build is a user build */
isUserBuild()61     public static boolean isUserBuild() {
62         return propertyEquals(BUILD_TYPE_PROPERTY, "user");
63     }
64 
65     /** Returns whether this build is built with dev-keys */
isDevKeysBuild()66     public static boolean isDevKeysBuild() {
67         return isBuildWithKeyType(TAG_DEV_KEYS);
68     }
69 
70     /** Returns whether this build is built with test-keys */
isTestKeysBuild()71     public static boolean isTestKeysBuild() {
72         return isBuildWithKeyType(TAG_TEST_KEYS);
73     }
74 
75     /**
76      * Return the CameraX extensions enabled property value. If the read-only property is unset,
77      * the default value returned will be 'false'.
78      */
areCameraXExtensionsEnabled()79     public static boolean areCameraXExtensionsEnabled() {
80         return getPropertyBoolean(CAMERAX_EXTENSIONS_ENABLED);
81     }
82 
83     /**
84      * Return the first API level for this product. If the read-only property is unset,
85      * this means the first API level is the current API level, and the current API level
86      * is returned.
87      */
getFirstApiLevel()88     public static int getFirstApiLevel() {
89         int firstApiLevel = getPropertyInt(FIRST_API_LEVEL);
90         return (firstApiLevel == INT_VALUE_IF_UNSET) ? Build.VERSION.SDK_INT : firstApiLevel;
91     }
92 
93     /**
94      * Return the API level that the VSR requirement must be fulfilled. It reads
95      * ro.vendor.api_level. If not provided for old devices, read ro.product.first_api_level,
96      * ro.board.api_level and ro.board.first_api_level to find the minimum required VSR api level of
97      * the DUT.
98      */
getVsrApiLevel()99     public static int getVsrApiLevel() {
100         int vendorApiLevel = getPropertyInt(VENDOR_API_LEVEL);
101         if (vendorApiLevel != INT_VALUE_IF_UNSET) {
102             return vendorApiLevel;
103         }
104         // Fallback to api level calculation for old devices.
105         String[] boardApiLevelProps = {BOARD_API_LEVEL, BOARD_FIRST_API_LEVEL};
106         for (String apiLevelProp : boardApiLevelProps) {
107             int apiLevel = getPropertyInt(apiLevelProp);
108             if (apiLevel != INT_VALUE_IF_UNSET) {
109                 return Math.min(apiLevel, getFirstApiLevel());
110             }
111         }
112         return getFirstApiLevel();
113     }
114 
115     /**
116      * Return the API level of the vendor partition. It will read the following properties in order
117      * and returns the value of the first defined property. If none of them are defined, or the
118      * value is a VERSION CODENAME, returns the current API level which is defined in
119      * API_LEVEL_CURRENT.
120      *
121      * <ul>
122      *   <li> ro.board.api_level
123      *   <li> ro.board.first_api_level
124      *   <li> ro.vndk.version
125      * </ul>
126      */
getVendorApiLevel()127     public static int getVendorApiLevel() {
128         String[] vendorApiLevelProps = {
129             // Use the properties in order.
130             BOARD_API_LEVEL, BOARD_FIRST_API_LEVEL, VNDK_VERSION,
131         };
132         for (String prop : vendorApiLevelProps) {
133             int apiLevel = getPropertyInt(prop);
134             if (apiLevel != INT_VALUE_IF_UNSET) {
135                 return apiLevel;
136             }
137         }
138         return API_LEVEL_CURRENT;
139     }
140 
141     /**
142      * Return whether the API level of the vendor partition is newer than the given API level.
143      */
isVendorApiLevelNewerThan(int apiLevel)144     public static boolean isVendorApiLevelNewerThan(int apiLevel) {
145         return getVendorApiLevel() > apiLevel;
146     }
147 
148     /**
149      * Return whether the API level of the vendor partition is same or newer than the
150      * given API level.
151      */
isVendorApiLevelAtLeast(int apiLevel)152     public static boolean isVendorApiLevelAtLeast(int apiLevel) {
153         return getVendorApiLevel() >= apiLevel;
154     }
155 
156     /**
157      * Return whether the VNDK version of the vendor partition is newer than the given API level.
158      * If the property is set to non-integer value, this means the vendor partition is using
159      * current API level and true is returned.
160      */
isVndkApiLevelNewerThan(int apiLevel)161     public static boolean isVndkApiLevelNewerThan(int apiLevel) {
162         int vndkApiLevel = getPropertyInt(VNDK_VERSION);
163         if (vndkApiLevel == INT_VALUE_IF_UNSET) {
164             return true;
165         }
166         return vndkApiLevel > apiLevel;
167     }
168 
169     /**
170      * Return whether the VNDK version of the vendor partition is same or newer than the
171      * given API level.
172      * If the property is set to non-integer value, this means the vendor partition is using
173      * current API level and true is returned.
174      */
isVndkApiLevelAtLeast(int apiLevel)175     public static boolean isVndkApiLevelAtLeast(int apiLevel) {
176         int vndkApiLevel = getPropertyInt(VNDK_VERSION);
177         if (vndkApiLevel == INT_VALUE_IF_UNSET) {
178             return true;
179         }
180         return vndkApiLevel >= apiLevel;
181     }
182 
183     /**
184      * Return the manufacturer of this product. If unset, return null.
185      */
getManufacturer()186     public static String getManufacturer() {
187         return getProperty(MANUFACTURER_PROPERTY);
188     }
189 
190     /** Returns a mapping from client ID names to client ID values */
getClientIds()191     public static Map<String, String> getClientIds() throws IOException {
192         Map<String,String> clientIds = new HashMap<>();
193         String queryOutput = SystemUtil.runShellCommand(
194                 InstrumentationRegistry.getInstrumentation(), GOOGLE_SETTINGS_QUERY);
195         for (String line : queryOutput.split("[\\r?\\n]+")) {
196             // Expected line format: "Row: 1 _id=123, name=<property_name>, value=<property_value>"
197             Pattern pattern = Pattern.compile("name=([a-z_]*), value=(.*)$");
198             Matcher matcher = pattern.matcher(line);
199             if (matcher.find()) {
200                 String name = matcher.group(1);
201                 String value = matcher.group(2);
202                 if (name.contains("client_id")) {
203                     clientIds.put(name, value); // only add name-value pair for client ids
204                 }
205             }
206         }
207         return clientIds;
208     }
209 
210     /** Returns whether the property exists on this device */
propertyExists(String property)211     public static boolean propertyExists(String property) {
212         return getProperty(property) != null;
213     }
214 
215     /** Returns whether the property value is equal to a given string */
propertyEquals(String property, String value)216     public static boolean propertyEquals(String property, String value) {
217         if (value == null) {
218             return !propertyExists(property); // null value implies property does not exist
219         }
220         return value.equals(getProperty(property));
221     }
222 
223     /**
224      * Returns whether the property value matches a given regular expression. The method uses
225      * String.matches(), requiring a complete match (i.e. expression matches entire value string)
226      */
propertyMatches(String property, String regex)227     public static boolean propertyMatches(String property, String regex) {
228         if (regex == null || regex.isEmpty()) {
229             // null or empty pattern implies property does not exist
230             return !propertyExists(property);
231         }
232         String value = getProperty(property);
233         return (value == null) ? false : value.matches(regex);
234     }
235 
236     /**
237      * Retrieves the desired boolean property, returning false if not found.
238      */
getPropertyBoolean(String property)239     public static boolean getPropertyBoolean(String property) {
240         String value = getProperty(property);
241         if (value == null) {
242             return false;
243         }
244         return Boolean.parseBoolean(value);
245     }
246 
247     /**
248      * Retrieves the desired integer property, returning INT_VALUE_IF_UNSET if not found.
249      */
getPropertyInt(String property)250     public static int getPropertyInt(String property) {
251         String value = getProperty(property);
252         if (value == null) {
253             return INT_VALUE_IF_UNSET;
254         }
255         try {
256             return Integer.parseInt(value);
257         } catch (NumberFormatException e) {
258             return INT_VALUE_IF_UNSET;
259         }
260     }
261 
262     /** Retrieves the desired property value in string form */
getProperty(String property)263     public static String getProperty(String property) {
264         Scanner scanner = null;
265         try {
266             Process process = new ProcessBuilder("getprop", property).start();
267             scanner = new Scanner(process.getInputStream());
268             String value = scanner.nextLine().trim();
269             return (value.isEmpty()) ? null : value;
270         } catch (IOException e) {
271             return null;
272         } finally {
273             if (scanner != null) {
274                 scanner.close();
275             }
276         }
277     }
278 
279     /** Retrieves a map of prop to value for all props with the given prefix */
getPropertiesWithPrefix(String prefix)280     public static Map<String, String> getPropertiesWithPrefix(String prefix) {
281         Map<String, String> result = new HashMap<>();
282         Pattern pattern = Pattern.compile("\\[(.*)\\]: \\[(.*)\\]");
283         Scanner scanner = null;
284         try {
285             Process process = new ProcessBuilder("getprop").start();
286             scanner = new Scanner(process.getInputStream());
287             while (scanner.hasNextLine()) {
288                 String line = scanner.nextLine().trim();
289                 Matcher matcher = pattern.matcher(line);
290                 if (matcher.find()) {
291                     String prop = matcher.group(1);
292                     String value = matcher.group(2);
293                     if (prop.startsWith(prefix)) {
294                         result.put(prop, value);
295                     }
296                 }
297             }
298             return result;
299         } catch (IOException e) {
300             return result;
301         } finally {
302             if (scanner != null) {
303                 scanner.close();
304             }
305         }
306     }
307 
308     /**
309      * Checks if the current build uses a specific type of key.
310      *
311      * @param keyType The type of key to check for (e.g., "dev-keys", "test-keys")
312      * @return true if the build uses the specified key type, false otherwise
313     */
isBuildWithKeyType(String keyType)314     private static boolean isBuildWithKeyType(String keyType) {
315         for (String tag : Build.TAGS.split(",")) {
316             if (keyType.equals(tag.trim())) {
317                 return true;
318             }
319         }
320         return false;
321     }
322 }
323