blob: e04285db75be0efd9cdc22fe898d8fde624af4ef [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.textclassifier;
import static android.view.textclassifier.TextClassifier.DEFAULT_LOG_TAG;
import android.annotation.Nullable;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Manages model files that are listed by the model files supplier.
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class ModelFileManager {
private final Object mLock = new Object();
private final Supplier<List<ModelFile>> mModelFileSupplier;
private List<ModelFile> mModelFiles;
public ModelFileManager(Supplier<List<ModelFile>> modelFileSupplier) {
mModelFileSupplier = Preconditions.checkNotNull(modelFileSupplier);
}
/**
* Returns an unmodifiable list of model files listed by the given model files supplier.
* <p>
* The result is cached.
*/
public List<ModelFile> listModelFiles() {
synchronized (mLock) {
if (mModelFiles == null) {
mModelFiles = Collections.unmodifiableList(mModelFileSupplier.get());
}
return mModelFiles;
}
}
/**
* Returns the best model file for the given localelist, {@code null} if nothing is found.
*
* @param localeList the required locales, use {@code null} if there is no preference.
*/
public ModelFile findBestModelFile(@Nullable LocaleList localeList) {
final String languages = localeList == null || localeList.isEmpty()
? LocaleList.getDefault().toLanguageTags()
: localeList.toLanguageTags();
final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(languages);
ModelFile bestModel = null;
for (ModelFile model : listModelFiles()) {
if (model.isAnyLanguageSupported(languageRangeList)) {
if (model.isPreferredTo(bestModel)) {
bestModel = model;
}
}
}
return bestModel;
}
/**
* Default implementation of the model file supplier.
*/
public static final class ModelFileSupplierImpl implements Supplier<List<ModelFile>> {
private final File mUpdatedModelFile;
private final File mFactoryModelDir;
private final Pattern mModelFilenamePattern;
private final Function<Integer, Integer> mVersionSupplier;
private final Function<Integer, String> mSupportedLocalesSupplier;
public ModelFileSupplierImpl(
File factoryModelDir,
String factoryModelFileNameRegex,
File updatedModelFile,
Function<Integer, Integer> versionSupplier,
Function<Integer, String> supportedLocalesSupplier) {
mUpdatedModelFile = Preconditions.checkNotNull(updatedModelFile);
mFactoryModelDir = Preconditions.checkNotNull(factoryModelDir);
mModelFilenamePattern = Pattern.compile(
Preconditions.checkNotNull(factoryModelFileNameRegex));
mVersionSupplier = Preconditions.checkNotNull(versionSupplier);
mSupportedLocalesSupplier = Preconditions.checkNotNull(supportedLocalesSupplier);
}
@Override
public List<ModelFile> get() {
final List<ModelFile> modelFiles = new ArrayList<>();
// The update model has the highest precedence.
if (mUpdatedModelFile.exists()) {
final ModelFile updatedModel = createModelFile(mUpdatedModelFile);
if (updatedModel != null) {
modelFiles.add(updatedModel);
}
}
// Factory models should never have overlapping locales, so the order doesn't matter.
if (mFactoryModelDir.exists() && mFactoryModelDir.isDirectory()) {
final File[] files = mFactoryModelDir.listFiles();
for (File file : files) {
final Matcher matcher = mModelFilenamePattern.matcher(file.getName());
if (matcher.matches() && file.isFile()) {
final ModelFile model = createModelFile(file);
if (model != null) {
modelFiles.add(model);
}
}
}
}
return modelFiles;
}
/** Returns null if the path did not point to a compatible model. */
@Nullable
private ModelFile createModelFile(File file) {
if (!file.exists()) {
return null;
}
ParcelFileDescriptor modelFd = null;
try {
modelFd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
if (modelFd == null) {
return null;
}
final int modelFdInt = modelFd.getFd();
final int version = mVersionSupplier.apply(modelFdInt);
final String supportedLocalesStr = mSupportedLocalesSupplier.apply(modelFdInt);
if (supportedLocalesStr.isEmpty()) {
Log.d(DEFAULT_LOG_TAG, "Ignoring " + file.getAbsolutePath());
return null;
}
final List<Locale> supportedLocales = new ArrayList<>();
for (String langTag : supportedLocalesStr.split(",")) {
supportedLocales.add(Locale.forLanguageTag(langTag));
}
return new ModelFile(
file,
version,
supportedLocales,
supportedLocalesStr,
ModelFile.LANGUAGE_INDEPENDENT.equals(supportedLocalesStr));
} catch (FileNotFoundException e) {
Log.e(DEFAULT_LOG_TAG, "Failed to find " + file.getAbsolutePath(), e);
return null;
} finally {
maybeCloseAndLogError(modelFd);
}
}
/**
* Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
*/
private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
if (fd == null) {
return;
}
try {
fd.close();
} catch (IOException e) {
Log.e(DEFAULT_LOG_TAG, "Error closing file.", e);
}
}
}
/**
* Describes TextClassifier model files on disk.
*/
public static final class ModelFile {
public static final String LANGUAGE_INDEPENDENT = "*";
private final File mFile;
private final int mVersion;
private final List<Locale> mSupportedLocales;
private final String mSupportedLocalesStr;
private final boolean mLanguageIndependent;
public ModelFile(File file, int version, List<Locale> supportedLocales,
String supportedLocalesStr,
boolean languageIndependent) {
mFile = Preconditions.checkNotNull(file);
mVersion = version;
mSupportedLocales = Preconditions.checkNotNull(supportedLocales);
mSupportedLocalesStr = Preconditions.checkNotNull(supportedLocalesStr);
mLanguageIndependent = languageIndependent;
}
/** Returns the absolute path to the model file. */
public String getPath() {
return mFile.getAbsolutePath();
}
/** Returns a name to use for id generation, effectively the name of the model file. */
public String getName() {
return mFile.getName();
}
/** Returns the version tag in the model's metadata. */
public int getVersion() {
return mVersion;
}
/** Returns whether the language supports any language in the given ranges. */
public boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) {
Preconditions.checkNotNull(languageRanges);
return mLanguageIndependent || Locale.lookup(languageRanges, mSupportedLocales) != null;
}
/** Returns an immutable lists of supported locales. */
public List<Locale> getSupportedLocales() {
return Collections.unmodifiableList(mSupportedLocales);
}
/** Returns the original supported locals string read from the model file. */
public String getSupportedLocalesStr() {
return mSupportedLocalesStr;
}
/**
* Returns if this model file is preferred to the given one.
*/
public boolean isPreferredTo(@Nullable ModelFile model) {
// A model is preferred to no model.
if (model == null) {
return true;
}
// A language-specific model is preferred to a language independent
// model.
if (!mLanguageIndependent && model.mLanguageIndependent) {
return true;
}
if (mLanguageIndependent && !model.mLanguageIndependent) {
return false;
}
// A higher-version model is preferred.
if (mVersion > model.getVersion()) {
return true;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(getPath());
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof ModelFile) {
final ModelFile otherModel = (ModelFile) other;
return TextUtils.equals(getPath(), otherModel.getPath());
}
return false;
}
@Override
public String toString() {
final StringJoiner localesJoiner = new StringJoiner(",");
for (Locale locale : mSupportedLocales) {
localesJoiner.add(locale.toLanguageTag());
}
return String.format(Locale.US,
"ModelFile { path=%s name=%s version=%d locales=%s }",
getPath(), getName(), mVersion, localesJoiner.toString());
}
}
}