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 17 package com.android.textclassifier.common; 18 19 import android.content.res.AssetFileDescriptor; 20 import android.content.res.AssetManager; 21 import android.os.LocaleList; 22 import android.os.ParcelFileDescriptor; 23 import com.android.textclassifier.common.ModelType.ModelTypeDef; 24 import com.android.textclassifier.common.logging.ResultIdUtils.ModelInfo; 25 import com.google.android.textclassifier.ActionsSuggestionsModel; 26 import com.google.android.textclassifier.AnnotatorModel; 27 import com.google.android.textclassifier.LangIdModel; 28 import com.google.common.annotations.VisibleForTesting; 29 import com.google.common.base.Function; 30 import com.google.common.base.Optional; 31 import com.google.common.base.Preconditions; 32 import com.google.common.collect.ImmutableList; 33 import java.io.File; 34 import java.io.IOException; 35 import java.util.Arrays; 36 import java.util.List; 37 import java.util.Locale; 38 import java.util.Objects; 39 import java.util.stream.Collectors; 40 import javax.annotation.Nullable; 41 42 /** Describes TextClassifier model files on disk. */ 43 public class ModelFile { 44 public static final String LANGUAGE_INDEPENDENT = "*"; 45 46 @ModelTypeDef public final String modelType; 47 public final String absolutePath; 48 public final int version; 49 public final LocaleList supportedLocales; 50 public final boolean languageIndependent; 51 public final boolean isAsset; 52 createFromRegularFile(File file, @ModelTypeDef String modelType)53 public static ModelFile createFromRegularFile(File file, @ModelTypeDef String modelType) 54 throws IOException { 55 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 56 try (AssetFileDescriptor afd = new AssetFileDescriptor(pfd, 0, file.length())) { 57 return createFromAssetFileDescriptor( 58 file.getAbsolutePath(), modelType, afd, /* isAsset= */ false); 59 } 60 } 61 createFromAsset( AssetManager assetManager, String absolutePath, @ModelTypeDef String modelType)62 public static ModelFile createFromAsset( 63 AssetManager assetManager, String absolutePath, @ModelTypeDef String modelType) 64 throws IOException { 65 try (AssetFileDescriptor assetFileDescriptor = assetManager.openFd(absolutePath)) { 66 return createFromAssetFileDescriptor( 67 absolutePath, modelType, assetFileDescriptor, /* isAsset= */ true); 68 } 69 } 70 createFromAssetFileDescriptor( String absolutePath, @ModelTypeDef String modelType, AssetFileDescriptor assetFileDescriptor, boolean isAsset)71 private static ModelFile createFromAssetFileDescriptor( 72 String absolutePath, 73 @ModelTypeDef String modelType, 74 AssetFileDescriptor assetFileDescriptor, 75 boolean isAsset) { 76 ModelInfoFetcher modelInfoFetcher = ModelInfoFetcher.create(modelType); 77 return new ModelFile( 78 modelType, 79 absolutePath, 80 modelInfoFetcher.getVersion(assetFileDescriptor), 81 modelInfoFetcher.getSupportedLocales(assetFileDescriptor), 82 isAsset); 83 } 84 85 @VisibleForTesting ModelFile( @odelTypeDef String modelType, String absolutePath, int version, String supportedLocaleTags, boolean isAsset)86 public ModelFile( 87 @ModelTypeDef String modelType, 88 String absolutePath, 89 int version, 90 String supportedLocaleTags, 91 boolean isAsset) { 92 this.modelType = modelType; 93 this.absolutePath = absolutePath; 94 this.version = version; 95 this.languageIndependent = LANGUAGE_INDEPENDENT.equals(supportedLocaleTags); 96 this.supportedLocales = 97 languageIndependent 98 ? LocaleList.getEmptyLocaleList() 99 : LocaleList.forLanguageTags(supportedLocaleTags); 100 this.isAsset = isAsset; 101 } 102 103 /** Returns if this model file is preferred to the given one. */ isPreferredTo(@ullable ModelFile model)104 public boolean isPreferredTo(@Nullable ModelFile model) { 105 // A model is preferred to no model. 106 if (model == null) { 107 return true; 108 } 109 110 // A language-specific model is preferred to a language independent 111 // model. 112 if (!languageIndependent && model.languageIndependent) { 113 return true; 114 } 115 if (languageIndependent && !model.languageIndependent) { 116 return false; 117 } 118 119 // A higher-version model is preferred. 120 if (version > model.version) { 121 return true; 122 } 123 return false; 124 } 125 126 /** Returns whether the language supports any language in the given ranges. */ isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges)127 public boolean isAnyLanguageSupported(List<Locale.LanguageRange> languageRanges) { 128 Preconditions.checkNotNull(languageRanges); 129 if (languageIndependent) { 130 return true; 131 } 132 List<String> supportedLocaleTags = Arrays.asList(supportedLocales.toLanguageTags().split(",")); 133 return Locale.lookupTag(languageRanges, supportedLocaleTags) != null; 134 } 135 open(AssetManager assetManager)136 public AssetFileDescriptor open(AssetManager assetManager) throws IOException { 137 if (isAsset) { 138 return assetManager.openFd(absolutePath); 139 } 140 File file = new File(absolutePath); 141 ParcelFileDescriptor parcelFileDescriptor = 142 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 143 return new AssetFileDescriptor(parcelFileDescriptor, 0, file.length()); 144 } 145 canWrite()146 public boolean canWrite() { 147 if (isAsset) { 148 return false; 149 } 150 return new File(absolutePath).canWrite(); 151 } 152 delete()153 public boolean delete() { 154 if (isAsset) { 155 throw new IllegalStateException("asset is read-only, deleting it is not allowed."); 156 } 157 return new File(absolutePath).delete(); 158 } 159 160 @Override equals(Object o)161 public boolean equals(Object o) { 162 if (this == o) { 163 return true; 164 } 165 if (!(o instanceof ModelFile)) { 166 return false; 167 } 168 ModelFile modelFile = (ModelFile) o; 169 return version == modelFile.version 170 && languageIndependent == modelFile.languageIndependent 171 && isAsset == modelFile.isAsset 172 && Objects.equals(modelType, modelFile.modelType) 173 && Objects.equals(absolutePath, modelFile.absolutePath) 174 && Objects.equals(supportedLocales, modelFile.supportedLocales); 175 } 176 177 @Override hashCode()178 public int hashCode() { 179 return Objects.hash( 180 modelType, absolutePath, version, supportedLocales, languageIndependent, isAsset); 181 } 182 toModelInfo()183 public ModelInfo toModelInfo() { 184 return new ModelInfo( 185 version, languageIndependent ? LANGUAGE_INDEPENDENT : supportedLocales.toLanguageTags()); 186 } 187 188 @Override toString()189 public String toString() { 190 return String.format( 191 Locale.US, 192 "ModelFile { type=%s path=%s version=%d locales=%s isAsset=%b}", 193 modelType, 194 absolutePath, 195 version, 196 languageIndependent ? LANGUAGE_INDEPENDENT : supportedLocales.toLanguageTags(), 197 isAsset); 198 } 199 toModelInfos(Optional<ModelFile>.... modelFiles)200 public static ImmutableList<Optional<ModelInfo>> toModelInfos(Optional<ModelFile>... modelFiles) { 201 return Arrays.stream(modelFiles) 202 .map(modelFile -> modelFile.transform(ModelFile::toModelInfo)) 203 .collect(Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf)); 204 } 205 206 /** Fetch metadata of a model file. */ 207 private static class ModelInfoFetcher { 208 private final Function<AssetFileDescriptor, Integer> versionFetcher; 209 private final Function<AssetFileDescriptor, String> supportedLocalesFetcher; 210 ModelInfoFetcher( Function<AssetFileDescriptor, Integer> versionFetcher, Function<AssetFileDescriptor, String> supportedLocalesFetcher)211 private ModelInfoFetcher( 212 Function<AssetFileDescriptor, Integer> versionFetcher, 213 Function<AssetFileDescriptor, String> supportedLocalesFetcher) { 214 this.versionFetcher = versionFetcher; 215 this.supportedLocalesFetcher = supportedLocalesFetcher; 216 } 217 getVersion(AssetFileDescriptor assetFileDescriptor)218 int getVersion(AssetFileDescriptor assetFileDescriptor) { 219 return versionFetcher.apply(assetFileDescriptor); 220 } 221 getSupportedLocales(AssetFileDescriptor assetFileDescriptor)222 String getSupportedLocales(AssetFileDescriptor assetFileDescriptor) { 223 return supportedLocalesFetcher.apply(assetFileDescriptor); 224 } 225 create(@odelTypeDef String modelType)226 static ModelInfoFetcher create(@ModelTypeDef String modelType) { 227 switch (modelType) { 228 case ModelType.ANNOTATOR: 229 return new ModelInfoFetcher(AnnotatorModel::getVersion, AnnotatorModel::getLocales); 230 case ModelType.ACTIONS_SUGGESTIONS: 231 return new ModelInfoFetcher( 232 ActionsSuggestionsModel::getVersion, ActionsSuggestionsModel::getLocales); 233 case ModelType.LANG_ID: 234 return new ModelInfoFetcher( 235 LangIdModel::getVersion, afd -> ModelFile.LANGUAGE_INDEPENDENT); 236 default: // fall out 237 } 238 throw new IllegalStateException("Unsupported model types"); 239 } 240 } 241 } 242