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