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