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
17package com.android.inputmethod.latin;
18
19import android.content.ContentProviderClient;
20import android.content.Context;
21import android.content.res.AssetFileDescriptor;
22import android.content.res.Resources;
23import android.util.Log;
24
25import com.android.inputmethod.annotations.UsedForTesting;
26import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
27
28import java.io.File;
29import java.util.ArrayList;
30import java.util.LinkedList;
31import java.util.Locale;
32
33/**
34 * Factory for dictionary instances.
35 */
36public 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