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      */
createMainDictionaryFromManager(final Context context, final Locale locale, final boolean useFullEditDistance)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      */
killDictionary(final Context context, final AssetFileAddress f)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      */
createMainDictionaryFromManager(final Context context, final Locale locale)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      */
createReadOnlyBinaryDictionary(final Context context, final Locale locale)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
createDictionaryForTest(final AssetFileAddress[] dictionaryList, final boolean useFullEditDistance, Locale locale)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      */
isDictionaryAvailable(Context context, Locale locale)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