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.util.Log;
23
24import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
25
26import java.io.File;
27import java.util.ArrayList;
28import java.util.LinkedList;
29import java.util.Locale;
30
31/**
32 * Factory for dictionary instances.
33 */
34public 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     */
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     */
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     */
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