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