/* * Copyright (C) 2011 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 com.android.inputmethod.latin; import android.content.ContentProviderClient; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.util.Log; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import java.io.File; import java.util.ArrayList; import java.util.LinkedList; import java.util.Locale; /** * Factory for dictionary instances. */ public final class DictionaryFactory { private static final String TAG = DictionaryFactory.class.getSimpleName(); /** * Initializes a main dictionary collection from a dictionary pack, with explicit flags. * * This searches for a content provider providing a dictionary pack for the specified * locale. If none is found, it falls back to the built-in dictionary - if any. * @param context application context for reading resources * @param locale the locale for which to create the dictionary * @return an initialized instance of DictionaryCollection */ public static DictionaryCollection createMainDictionaryFromManager(final Context context, final Locale locale) { if (null == locale) { Log.e(TAG, "No locale defined for dictionary"); return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, createReadOnlyBinaryDictionary(context, locale)); } final LinkedList dictList = new LinkedList<>(); final ArrayList assetFileList = BinaryDictionaryGetter.getDictionaryFiles(locale, context, true); if (null != assetFileList) { for (final AssetFileAddress f : assetFileList) { final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength, false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN); if (readOnlyBinaryDictionary.isValidDictionary()) { dictList.add(readOnlyBinaryDictionary); } else { readOnlyBinaryDictionary.close(); // Prevent this dictionary to do any further harm. killDictionary(context, f); } } } // If the list is empty, that means we should not use any dictionary (for example, the user // explicitly disabled the main dictionary), so the following is okay. dictList is never // null, but if for some reason it is, DictionaryCollection handles it gracefully. return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList); } /** * Kills a dictionary so that it is never used again, if possible. * @param context The context to contact the dictionary provider, if possible. * @param f A file address to the dictionary to kill. */ public static void killDictionary(final Context context, final AssetFileAddress f) { if (f.pointsToPhysicalFile()) { f.deleteUnderlyingFile(); // Warn the dictionary provider if the dictionary came from there. final ContentProviderClient providerClient; try { providerClient = context.getContentResolver().acquireContentProviderClient( BinaryDictionaryFileDumper.getProviderUriBuilder("").build()); } catch (final SecurityException e) { Log.e(TAG, "No permission to communicate with the dictionary provider", e); return; } if (null == providerClient) { Log.e(TAG, "Can't establish communication with the dictionary provider"); return; } final String wordlistId = DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName()); // TODO: this is a reasonable last resort, but it is suboptimal. // The following will remove the entry for this dictionary with the dictionary // provider. When the metadata is downloaded again, we will try downloading it // again. // However, in the practice that will mean the user will find themselves without // the new dictionary. That's fine for languages where it's included in the APK, // but for other languages it will leave the user without a dictionary at all until // the next update, which may be a few days away. // Ideally, we would trigger a new download right away, and use increasing retry // delays for this particular id/version combination. // Then again, this is expected to only ever happen in case of human mistake. If // the wrong file is on the server, the following is still doing the right thing. // If it's a file left over from the last version however, it's not great. BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider( providerClient, context.getString(R.string.dictionary_pack_client_id), wordlistId); } } /** * Initializes a read-only binary dictionary from a raw resource file * @param context application context for reading resources * @param locale the locale to use for the resource * @return an initialized instance of ReadOnlyBinaryDictionary */ private static ReadOnlyBinaryDictionary createReadOnlyBinaryDictionary(final Context context, final Locale locale) { AssetFileDescriptor afd = null; try { final int resId = DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale( context.getResources(), locale); if (0 == resId) return null; afd = context.getResources().openRawResourceFd(resId); if (afd == null) { Log.e(TAG, "Found the resource but it is compressed. resId=" + resId); return null; } final String sourceDir = context.getApplicationInfo().sourceDir; final File packagePath = new File(sourceDir); // TODO: Come up with a way to handle a directory. if (!packagePath.isFile()) { Log.e(TAG, "sourceDir is not a file: " + sourceDir); return null; } return new ReadOnlyBinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(), false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN); } catch (android.content.res.Resources.NotFoundException e) { Log.e(TAG, "Could not find the resource"); return null; } finally { if (null != afd) { try { afd.close(); } catch (java.io.IOException e) { /* IOException on close ? What am I supposed to do ? */ } } } } }