1 /* 2 * Copyright (C) 2011 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.inputmethod.latin; 18 19 import android.content.ContentProviderClient; 20 import android.content.Context; 21 import android.content.res.AssetFileDescriptor; 22 import android.content.res.Resources; 23 import android.util.Log; 24 25 import com.android.inputmethod.annotations.UsedForTesting; 26 import com.android.inputmethod.latin.utils.DictionaryInfoUtils; 27 28 import java.io.File; 29 import java.util.ArrayList; 30 import java.util.LinkedList; 31 import java.util.Locale; 32 33 /** 34 * Factory for dictionary instances. 35 */ 36 public final class DictionaryFactory { 37 private static final String TAG = DictionaryFactory.class.getSimpleName(); 38 39 /** 40 * Initializes a main dictionary collection from a dictionary pack, with explicit flags. 41 * 42 * This searches for a content provider providing a dictionary pack for the specified 43 * locale. If none is found, it falls back to the built-in dictionary - if any. 44 * @param context application context for reading resources 45 * @param locale the locale for which to create the dictionary 46 * @param useFullEditDistance whether to use the full edit distance in suggestions 47 * @return an initialized instance of DictionaryCollection 48 */ 49 public static DictionaryCollection createMainDictionaryFromManager(final Context context, 50 final Locale locale, final boolean useFullEditDistance) { 51 if (null == locale) { 52 Log.e(TAG, "No locale defined for dictionary"); 53 return new DictionaryCollection(Dictionary.TYPE_MAIN, 54 createReadOnlyBinaryDictionary(context, locale)); 55 } 56 57 final LinkedList<Dictionary> dictList = new LinkedList<>(); 58 final ArrayList<AssetFileAddress> assetFileList = 59 BinaryDictionaryGetter.getDictionaryFiles(locale, context); 60 if (null != assetFileList) { 61 for (final AssetFileAddress f : assetFileList) { 62 final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = 63 new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength, 64 useFullEditDistance, locale, Dictionary.TYPE_MAIN); 65 if (readOnlyBinaryDictionary.isValidDictionary()) { 66 dictList.add(readOnlyBinaryDictionary); 67 } else { 68 readOnlyBinaryDictionary.close(); 69 // Prevent this dictionary to do any further harm. 70 killDictionary(context, f); 71 } 72 } 73 } 74 75 // If the list is empty, that means we should not use any dictionary (for example, the user 76 // explicitly disabled the main dictionary), so the following is okay. dictList is never 77 // null, but if for some reason it is, DictionaryCollection handles it gracefully. 78 return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList); 79 } 80 81 /** 82 * Kills a dictionary so that it is never used again, if possible. 83 * @param context The context to contact the dictionary provider, if possible. 84 * @param f A file address to the dictionary to kill. 85 */ 86 private static void killDictionary(final Context context, final AssetFileAddress f) { 87 if (f.pointsToPhysicalFile()) { 88 f.deleteUnderlyingFile(); 89 // Warn the dictionary provider if the dictionary came from there. 90 final ContentProviderClient providerClient; 91 try { 92 providerClient = context.getContentResolver().acquireContentProviderClient( 93 BinaryDictionaryFileDumper.getProviderUriBuilder("").build()); 94 } catch (final SecurityException e) { 95 Log.e(TAG, "No permission to communicate with the dictionary provider", e); 96 return; 97 } 98 if (null == providerClient) { 99 Log.e(TAG, "Can't establish communication with the dictionary provider"); 100 return; 101 } 102 final String wordlistId = 103 DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName()); 104 if (null != wordlistId) { 105 // TODO: this is a reasonable last resort, but it is suboptimal. 106 // The following will remove the entry for this dictionary with the dictionary 107 // provider. When the metadata is downloaded again, we will try downloading it 108 // again. 109 // However, in the practice that will mean the user will find themselves without 110 // the new dictionary. That's fine for languages where it's included in the APK, 111 // but for other languages it will leave the user without a dictionary at all until 112 // the next update, which may be a few days away. 113 // Ideally, we would trigger a new download right away, and use increasing retry 114 // delays for this particular id/version combination. 115 // Then again, this is expected to only ever happen in case of human mistake. If 116 // the wrong file is on the server, the following is still doing the right thing. 117 // If it's a file left over from the last version however, it's not great. 118 BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider( 119 providerClient, 120 context.getString(R.string.dictionary_pack_client_id), 121 wordlistId); 122 } 123 } 124 } 125 126 /** 127 * Initializes a main dictionary collection from a dictionary pack, with default flags. 128 * 129 * This searches for a content provider providing a dictionary pack for the specified 130 * locale. If none is found, it falls back to the built-in dictionary, if any. 131 * @param context application context for reading resources 132 * @param locale the locale for which to create the dictionary 133 * @return an initialized instance of DictionaryCollection 134 */ 135 public static DictionaryCollection createMainDictionaryFromManager(final Context context, 136 final Locale locale) { 137 return createMainDictionaryFromManager(context, locale, false /* useFullEditDistance */); 138 } 139 140 /** 141 * Initializes a read-only binary dictionary from a raw resource file 142 * @param context application context for reading resources 143 * @param locale the locale to use for the resource 144 * @return an initialized instance of ReadOnlyBinaryDictionary 145 */ 146 protected static ReadOnlyBinaryDictionary createReadOnlyBinaryDictionary(final Context context, 147 final Locale locale) { 148 AssetFileDescriptor afd = null; 149 try { 150 final int resId = DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale( 151 context.getResources(), locale); 152 if (0 == resId) return null; 153 afd = context.getResources().openRawResourceFd(resId); 154 if (afd == null) { 155 Log.e(TAG, "Found the resource but it is compressed. resId=" + resId); 156 return null; 157 } 158 final String sourceDir = context.getApplicationInfo().sourceDir; 159 final File packagePath = new File(sourceDir); 160 // TODO: Come up with a way to handle a directory. 161 if (!packagePath.isFile()) { 162 Log.e(TAG, "sourceDir is not a file: " + sourceDir); 163 return null; 164 } 165 return new ReadOnlyBinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(), 166 false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN); 167 } catch (android.content.res.Resources.NotFoundException e) { 168 Log.e(TAG, "Could not find the resource"); 169 return null; 170 } finally { 171 if (null != afd) { 172 try { 173 afd.close(); 174 } catch (java.io.IOException e) { 175 /* IOException on close ? What am I supposed to do ? */ 176 } 177 } 178 } 179 } 180 181 /** 182 * Create a dictionary from passed data. This is intended for unit tests only. 183 * @param dictionaryList the list of files to read, with their offsets and lengths 184 * @param useFullEditDistance whether to use the full edit distance in suggestions 185 * @return the created dictionary, or null. 186 */ 187 @UsedForTesting 188 public static Dictionary createDictionaryForTest(final AssetFileAddress[] dictionaryList, 189 final boolean useFullEditDistance, Locale locale) { 190 final DictionaryCollection dictionaryCollection = 191 new DictionaryCollection(Dictionary.TYPE_MAIN); 192 for (final AssetFileAddress address : dictionaryList) { 193 final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary( 194 address.mFilename, address.mOffset, address.mLength, useFullEditDistance, 195 locale, Dictionary.TYPE_MAIN); 196 dictionaryCollection.addDictionary(readOnlyBinaryDictionary); 197 } 198 return dictionaryCollection; 199 } 200 201 /** 202 * Find out whether a dictionary is available for this locale. 203 * @param context the context on which to check resources. 204 * @param locale the locale to check for. 205 * @return whether a (non-placeholder) dictionary is available or not. 206 */ 207 public static boolean isDictionaryAvailable(Context context, Locale locale) { 208 final Resources res = context.getResources(); 209 return 0 != DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale( 210 res, locale); 211 } 212 } 213