Home | History | Annotate | Download | only in latin
      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