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