1 /* 2 * Copyright (C) 2015 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 package com.android.compatibility.common.deviceinfo; 17 18 import static java.util.stream.Collectors.toList; 19 import static java.util.stream.Collectors.toSet; 20 21 import android.icu.util.ULocale; 22 import android.icu.util.VersionInfo; 23 import android.util.Log; 24 import androidx.annotation.Nullable; 25 import com.android.compatibility.common.util.DeviceInfoStore; 26 import com.google.common.base.Strings; 27 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.nio.MappedByteBuffer; 32 import java.nio.channels.FileChannel; 33 import java.nio.file.Files; 34 import java.nio.file.Path; 35 import java.nio.file.Paths; 36 import java.security.MessageDigest; 37 import java.security.NoSuchAlgorithmException; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.List; 41 import java.util.Set; 42 import java.util.regex.Matcher; 43 import java.util.regex.Pattern; 44 import java.util.stream.Stream; 45 46 /** 47 * Locale device info collector. 48 */ 49 public final class LocaleDeviceInfo extends DeviceInfo { 50 private static final String TAG = "LocaleDeviceInfo"; 51 52 private static final Pattern HYPHEN_BINARY_PATTERN = Pattern.compile("hyph-(.*?).hyb"); 53 private static final String HYPHEN_BINARY_LOCATION = "/system/usr/hyphen-data/"; 54 55 @Override collectDeviceInfo(DeviceInfoStore store)56 protected void collectDeviceInfo(DeviceInfoStore store) throws Exception { 57 List<String> locales = Arrays.asList( 58 getInstrumentation().getContext().getAssets().getLocales()); 59 if (locales.isEmpty()) { 60 // default locale 61 locales.add("en_US"); 62 } 63 store.addListResult("locale", locales); 64 65 List<String> icuLocales = Arrays.stream(ULocale.getAvailableLocales()) 66 .map((uLocale -> uLocale.toLanguageTag())) 67 .collect(toList()); 68 if (icuLocales.isEmpty()) { 69 // default locale 70 icuLocales.add(ULocale.US.toLanguageTag()); 71 } 72 store.addListResult("icu_locale", icuLocales); 73 74 // Collect hyphenation supported locale 75 List<String> hyphenLocalesList = new ArrayList<>(); 76 // Retrieve locale from the file name of binary 77 try (Stream<Path> stream = Files.walk(Paths.get(HYPHEN_BINARY_LOCATION))) { 78 hyphenLocalesList = stream 79 .filter(file -> !Files.isDirectory(file)) 80 .map(Path::getFileName) 81 .map(Path::toString) 82 .filter(HYPHEN_BINARY_PATTERN.asPredicate()) 83 .map(s -> { 84 Matcher matcher = HYPHEN_BINARY_PATTERN.matcher(s); 85 return matcher.find() ? Strings.nullToEmpty(matcher.group(1)) : ""; 86 }) 87 .sorted() 88 .collect(toList()); 89 } catch (IOException e) { 90 Log.w(TAG,"Hyphenation binary folder is not exist" , e); 91 } 92 store.addListResult("hyphenation_locale", hyphenLocalesList); 93 94 collectLocaleDataFilesInfo(store); 95 } 96 97 /** 98 * Collect the fingerprints of ICU data files. On AOSP build, there are 5 data files. 99 * The example paths are /apex/com.android.tzdata/etc/icu/zoneinfo64.res and 100 * /apex/com.android.i18n/etc/icu/icudt65l.dat 101 */ collectLocaleDataFilesInfo(DeviceInfoStore store)102 private void collectLocaleDataFilesInfo(DeviceInfoStore store) throws IOException { 103 int icuVersion = VersionInfo.ICU_VERSION.getMajor(); 104 105 String icuDatFilePath = "/apex/com.android.i18n/etc/icu/icudt" + icuVersion + "l.dat"; 106 Stream<String> tzdataModuleResFiles = 107 Stream.of( 108 "metaZones.res", 109 "zoneinfo64.res", 110 "timezoneTypes.res", 111 "windowsZones.res") 112 .map(fileName -> "/apex/com.android.tzdata/etc/icu/" + fileName); 113 Set<File> fixedIcuFilesPaths = 114 Stream.concat( 115 Stream.of(icuDatFilePath), 116 tzdataModuleResFiles) 117 .map(File::new) 118 .collect(toSet()); 119 120 // This property has been deprecated since Android 12. The property will not work if this 121 // app targets SDK level 31 or higher. Thus, we add the above fixedDatPaths in case that 122 // the property is not working. When this comment was written, this CTS app still targets 123 // SDK level 23. 124 String prop = System.getProperty("android.icu.impl.ICUBinary.dataPath"); 125 store.startArray("icu_data_file_info"); 126 127 List<File> icuFiles = new ArrayList<>(); 128 if (prop != null) { 129 String[] dataDirs = prop.split(":"); 130 // List all ".dat" files in the directories. 131 icuFiles = Arrays.stream(dataDirs) 132 .filter((dir) -> dir != null && !dir.isEmpty()) 133 .map((dir) -> new File(dir)) 134 .filter((f) -> f.canRead() && f.isDirectory()) 135 .map((f) -> f.listFiles()) 136 .filter((files) -> files != null) 137 .flatMap(files -> Stream.of(files)) 138 .collect(toList()); 139 } 140 141 icuFiles.addAll(fixedIcuFilesPaths); 142 143 // Keep the readable paths only. 144 icuFiles = icuFiles.stream() 145 .distinct() 146 .filter((f) -> f != null && f.canRead() && isIcuFile(f)) 147 .collect(toList()); 148 149 for (File icuFile : icuFiles) { 150 String sha256Hash = sha256(icuFile); 151 152 store.startGroup(); 153 store.addResult("file_path", icuFile.getPath()); 154 // Still store the null hash to indicate an error occurring when obtaining the hash. 155 store.addResult("file_sha256", sha256Hash); 156 store.endGroup(); 157 } 158 store.endArray(); 159 } 160 isIcuFile(File file)161 private static boolean isIcuFile(File file) { 162 return file.getName().endsWith(".dat") || file.getName().endsWith(".res"); 163 } 164 sha256(File file)165 public static @Nullable String sha256(File file) { 166 try (FileInputStream in = new FileInputStream(file); 167 FileChannel fileChannel = in.getChannel()) { 168 169 MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, 170 fileChannel.size()); 171 172 MessageDigest md = MessageDigest.getInstance("SHA-256"); 173 md.update(mappedByteBuffer); 174 175 byte[] digest = md.digest(); 176 StringBuilder sb = new StringBuilder(digest.length * 2); 177 for(int i = 0; i < digest.length; i++){ 178 sb.append(Character.forDigit((digest[i] >> 4) & 0xF, 16)); 179 sb.append(Character.forDigit((digest[i] & 0xF), 16)); 180 } 181 return sb.toString(); 182 } catch (IOException | NoSuchAlgorithmException e) { 183 Log.w(TAG, String.format("Can't obtain the hash of file: %s", file), e); 184 return null; 185 } 186 } 187 } 188