blob: 0a4ff5d559ab6f737a34a510063450ef17cf6f3d [file] [log] [blame]
Tony Makba228422018-10-25 21:30:40 +01001/*
2 * Copyright (C) 2018 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 */
16package android.view.textclassifier;
17
18import static android.view.textclassifier.TextClassifier.DEFAULT_LOG_TAG;
19
20import android.annotation.Nullable;
21import android.os.LocaleList;
22import android.os.ParcelFileDescriptor;
23import android.text.TextUtils;
24
25import com.android.internal.annotations.VisibleForTesting;
Tony Makba228422018-10-25 21:30:40 +010026
27import java.io.File;
28import java.io.FileNotFoundException;
29import java.io.IOException;
30import java.util.ArrayList;
31import java.util.Collections;
32import java.util.List;
33import java.util.Locale;
34import java.util.Objects;
35import java.util.StringJoiner;
36import java.util.function.Function;
37import java.util.function.Supplier;
38import java.util.regex.Matcher;
39import java.util.regex.Pattern;
40
41/**
42 * Manages model files that are listed by the model files supplier.
43 * @hide
44 */
45@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
46public final class ModelFileManager {
47 private final Object mLock = new Object();
48 private final Supplier<List<ModelFile>> mModelFileSupplier;
49
50 private List<ModelFile> mModelFiles;
51
52 public ModelFileManager(Supplier<List<ModelFile>> modelFileSupplier) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +000053 mModelFileSupplier = Objects.requireNonNull(modelFileSupplier);
Tony Makba228422018-10-25 21:30:40 +010054 }
55
56 /**
57 * Returns an unmodifiable list of model files listed by the given model files supplier.
58 * <p>
59 * The result is cached.
60 */
61 public List<ModelFile> listModelFiles() {
62 synchronized (mLock) {
63 if (mModelFiles == null) {
64 mModelFiles = Collections.unmodifiableList(mModelFileSupplier.get());
65 }
66 return mModelFiles;
67 }
68 }
69
70 /**
71 * Returns the best model file for the given localelist, {@code null} if nothing is found.
72 *
73 * @param localeList the required locales, use {@code null} if there is no preference.
74 */
75 public ModelFile findBestModelFile(@Nullable LocaleList localeList) {
Tony Makba228422018-10-25 21:30:40 +010076 final String languages = localeList == null || localeList.isEmpty()
77 ? LocaleList.getDefault().toLanguageTags()
Tony Makadbebcc2018-10-30 18:59:06 +000078 : localeList.toLanguageTags();
Tony Makba228422018-10-25 21:30:40 +010079 final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
80
81 ModelFile bestModel = null;
82 for (ModelFile model : listModelFiles()) {
83 if (model.isAnyLanguageSupported(languageRangeList)) {
84 if (model.isPreferredTo(bestModel)) {
85 bestModel = model;
86 }
87 }
88 }
89 return bestModel;
90 }
91
92 /**
93 * Default implementation of the model file supplier.
94 */
95 public static final class ModelFileSupplierImpl implements Supplier<List<ModelFile>> {
96 private final File mUpdatedModelFile;
97 private final File mFactoryModelDir;
98 private final Pattern mModelFilenamePattern;
99 private final Function<Integer, Integer> mVersionSupplier;
100 private final Function<Integer, String> mSupportedLocalesSupplier;
101
102 public ModelFileSupplierImpl(
103 File factoryModelDir,
104 String factoryModelFileNameRegex,
105 File updatedModelFile,
106 Function<Integer, Integer> versionSupplier,
107 Function<Integer, String> supportedLocalesSupplier) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000108 mUpdatedModelFile = Objects.requireNonNull(updatedModelFile);
109 mFactoryModelDir = Objects.requireNonNull(factoryModelDir);
Tony Makba228422018-10-25 21:30:40 +0100110 mModelFilenamePattern = Pattern.compile(
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000111 Objects.requireNonNull(factoryModelFileNameRegex));
112 mVersionSupplier = Objects.requireNonNull(versionSupplier);
113 mSupportedLocalesSupplier = Objects.requireNonNull(supportedLocalesSupplier);
Tony Makba228422018-10-25 21:30:40 +0100114 }
115
116 @Override
117 public List<ModelFile> get() {
118 final List<ModelFile> modelFiles = new ArrayList<>();
119 // The update model has the highest precedence.
120 if (mUpdatedModelFile.exists()) {
121 final ModelFile updatedModel = createModelFile(mUpdatedModelFile);
122 if (updatedModel != null) {
123 modelFiles.add(updatedModel);
124 }
125 }
126 // Factory models should never have overlapping locales, so the order doesn't matter.
127 if (mFactoryModelDir.exists() && mFactoryModelDir.isDirectory()) {
128 final File[] files = mFactoryModelDir.listFiles();
129 for (File file : files) {
130 final Matcher matcher = mModelFilenamePattern.matcher(file.getName());
131 if (matcher.matches() && file.isFile()) {
132 final ModelFile model = createModelFile(file);
133 if (model != null) {
134 modelFiles.add(model);
135 }
136 }
137 }
138 }
139 return modelFiles;
140 }
141
142 /** Returns null if the path did not point to a compatible model. */
143 @Nullable
144 private ModelFile createModelFile(File file) {
145 if (!file.exists()) {
146 return null;
147 }
148 ParcelFileDescriptor modelFd = null;
149 try {
150 modelFd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
151 if (modelFd == null) {
152 return null;
153 }
154 final int modelFdInt = modelFd.getFd();
155 final int version = mVersionSupplier.apply(modelFdInt);
156 final String supportedLocalesStr = mSupportedLocalesSupplier.apply(modelFdInt);
157 if (supportedLocalesStr.isEmpty()) {
158 Log.d(DEFAULT_LOG_TAG, "Ignoring " + file.getAbsolutePath());
159 return null;
160 }
161 final List<Locale> supportedLocales = new ArrayList<>();
162 for (String langTag : supportedLocalesStr.split(",")) {
163 supportedLocales.add(Locale.forLanguageTag(langTag));
164 }
165 return new ModelFile(
166 file,
167 version,
168 supportedLocales,
Tony Mak20fe1872019-03-22 15:35:15 +0000169 supportedLocalesStr,
Tony Makba228422018-10-25 21:30:40 +0100170 ModelFile.LANGUAGE_INDEPENDENT.equals(supportedLocalesStr));
171 } catch (FileNotFoundException e) {
172 Log.e(DEFAULT_LOG_TAG, "Failed to find " + file.getAbsolutePath(), e);
173 return null;
174 } finally {
175 maybeCloseAndLogError(modelFd);
176 }
177 }
178
179 /**
180 * Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
181 */
182 private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
183 if (fd == null) {
184 return;
185 }
186 try {
187 fd.close();
188 } catch (IOException e) {
189 Log.e(DEFAULT_LOG_TAG, "Error closing file.", e);
190 }
191 }
192
193 }
194
195 /**
196 * Describes TextClassifier model files on disk.
197 */
198 public static final class ModelFile {
199 public static final String LANGUAGE_INDEPENDENT = "*";
200
201 private final File mFile;
202 private final int mVersion;
203 private final List<Locale> mSupportedLocales;
Tony Mak20fe1872019-03-22 15:35:15 +0000204 private final String mSupportedLocalesStr;
Tony Makba228422018-10-25 21:30:40 +0100205 private final boolean mLanguageIndependent;
206
207 public ModelFile(File file, int version, List<Locale> supportedLocales,
Tony Mak20fe1872019-03-22 15:35:15 +0000208 String supportedLocalesStr,
Tony Makba228422018-10-25 21:30:40 +0100209 boolean languageIndependent) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000210 mFile = Objects.requireNonNull(file);
Tony Makba228422018-10-25 21:30:40 +0100211 mVersion = version;
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000212 mSupportedLocales = Objects.requireNonNull(supportedLocales);
213 mSupportedLocalesStr = Objects.requireNonNull(supportedLocalesStr);
Tony Makba228422018-10-25 21:30:40 +0100214 mLanguageIndependent = languageIndependent;
215 }
216
217 /** Returns the absolute path to the model file. */
218 public String getPath() {
219 return mFile.getAbsolutePath();
220 }
221
222 /** Returns a name to use for id generation, effectively the name of the model file. */
223 public String getName() {
224 return mFile.getName();
225 }
226
227 /** Returns the version tag in the model's metadata. */
228 public int getVersion() {
229 return mVersion;
230 }
231
232 /** Returns whether the language supports any language in the given ranges. */
233 public boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000234 Objects.requireNonNull(languageRanges);
Tony Makba228422018-10-25 21:30:40 +0100235 return mLanguageIndependent || Locale.lookup(languageRanges, mSupportedLocales) != null;
236 }
237
238 /** Returns an immutable lists of supported locales. */
239 public List<Locale> getSupportedLocales() {
240 return Collections.unmodifiableList(mSupportedLocales);
241 }
242
Tony Mak20fe1872019-03-22 15:35:15 +0000243 /** Returns the original supported locals string read from the model file. */
244 public String getSupportedLocalesStr() {
245 return mSupportedLocalesStr;
246 }
247
Tony Makba228422018-10-25 21:30:40 +0100248 /**
249 * Returns if this model file is preferred to the given one.
250 */
251 public boolean isPreferredTo(@Nullable ModelFile model) {
252 // A model is preferred to no model.
253 if (model == null) {
254 return true;
255 }
256
257 // A language-specific model is preferred to a language independent
258 // model.
259 if (!mLanguageIndependent && model.mLanguageIndependent) {
260 return true;
261 }
Tony Mak9d209612018-11-14 21:09:03 +0000262 if (mLanguageIndependent && !model.mLanguageIndependent) {
263 return false;
264 }
Tony Makba228422018-10-25 21:30:40 +0100265
266 // A higher-version model is preferred.
267 if (mVersion > model.getVersion()) {
268 return true;
269 }
270 return false;
271 }
272
273 @Override
274 public int hashCode() {
275 return Objects.hash(getPath());
276 }
277
278 @Override
279 public boolean equals(Object other) {
280 if (this == other) {
281 return true;
282 }
283 if (other instanceof ModelFile) {
284 final ModelFile otherModel = (ModelFile) other;
285 return TextUtils.equals(getPath(), otherModel.getPath());
286 }
287 return false;
288 }
289
290 @Override
291 public String toString() {
292 final StringJoiner localesJoiner = new StringJoiner(",");
293 for (Locale locale : mSupportedLocales) {
294 localesJoiner.add(locale.toLanguageTag());
295 }
296 return String.format(Locale.US,
297 "ModelFile { path=%s name=%s version=%d locales=%s }",
298 getPath(), getName(), mVersion, localesJoiner.toString());
299 }
300 }
301}