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