1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.content.Context;
20import android.content.res.AssetFileDescriptor;
21import android.content.res.Resources;
22import android.util.Log;
23
24import java.io.File;
25import java.util.ArrayList;
26import java.util.LinkedList;
27import java.util.Locale;
28
29/**
30 * Factory for dictionary instances.
31 */
32public final class DictionaryFactory {
33    private static final String TAG = DictionaryFactory.class.getSimpleName();
34    // This class must be located in the same package as LatinIME.java.
35    private static final String RESOURCE_PACKAGE_NAME =
36            DictionaryFactory.class.getPackage().getName();
37
38    /**
39     * Initializes a main dictionary collection from a dictionary pack, with explicit flags.
40     *
41     * This searches for a content provider providing a dictionary pack for the specified
42     * locale. If none is found, it falls back to the built-in dictionary - if any.
43     * @param context application context for reading resources
44     * @param locale the locale for which to create the dictionary
45     * @param useFullEditDistance whether to use the full edit distance in suggestions
46     * @return an initialized instance of DictionaryCollection
47     */
48    public static DictionaryCollection createMainDictionaryFromManager(final Context context,
49            final Locale locale, final boolean useFullEditDistance) {
50        if (null == locale) {
51            Log.e(TAG, "No locale defined for dictionary");
52            return new DictionaryCollection(Dictionary.TYPE_MAIN,
53                    createBinaryDictionary(context, locale));
54        }
55
56        final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList();
57        final ArrayList<AssetFileAddress> assetFileList =
58                BinaryDictionaryGetter.getDictionaryFiles(locale, context);
59        if (null != assetFileList) {
60            for (final AssetFileAddress f : assetFileList) {
61                final BinaryDictionary binaryDictionary =
62                        new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength,
63                                useFullEditDistance, locale, Dictionary.TYPE_MAIN);
64                if (binaryDictionary.isValidDictionary()) {
65                    dictList.add(binaryDictionary);
66                }
67            }
68        }
69
70        // If the list is empty, that means we should not use any dictionary (for example, the user
71        // explicitly disabled the main dictionary), so the following is okay. dictList is never
72        // null, but if for some reason it is, DictionaryCollection handles it gracefully.
73        return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList);
74    }
75
76    /**
77     * Initializes a main dictionary collection from a dictionary pack, with default flags.
78     *
79     * This searches for a content provider providing a dictionary pack for the specified
80     * locale. If none is found, it falls back to the built-in dictionary, if any.
81     * @param context application context for reading resources
82     * @param locale the locale for which to create the dictionary
83     * @return an initialized instance of DictionaryCollection
84     */
85    public static DictionaryCollection createMainDictionaryFromManager(final Context context,
86            final Locale locale) {
87        return createMainDictionaryFromManager(context, locale, false /* useFullEditDistance */);
88    }
89
90    /**
91     * Initializes a dictionary from a raw resource file
92     * @param context application context for reading resources
93     * @param locale the locale to use for the resource
94     * @return an initialized instance of BinaryDictionary
95     */
96    protected static BinaryDictionary createBinaryDictionary(final Context context,
97            final Locale locale) {
98        AssetFileDescriptor afd = null;
99        try {
100            final int resId =
101                    getMainDictionaryResourceIdIfAvailableForLocale(context.getResources(), locale);
102            if (0 == resId) return null;
103            afd = context.getResources().openRawResourceFd(resId);
104            if (afd == null) {
105                Log.e(TAG, "Found the resource but it is compressed. resId=" + resId);
106                return null;
107            }
108            final String sourceDir = context.getApplicationInfo().sourceDir;
109            final File packagePath = new File(sourceDir);
110            // TODO: Come up with a way to handle a directory.
111            if (!packagePath.isFile()) {
112                Log.e(TAG, "sourceDir is not a file: " + sourceDir);
113                return null;
114            }
115            return new BinaryDictionary(context, sourceDir, afd.getStartOffset(), afd.getLength(),
116                    false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
117        } catch (android.content.res.Resources.NotFoundException e) {
118            Log.e(TAG, "Could not find the resource");
119            return null;
120        } finally {
121            if (null != afd) {
122                try {
123                    afd.close();
124                } catch (java.io.IOException e) {
125                    /* IOException on close ? What am I supposed to do ? */
126                }
127            }
128        }
129    }
130
131    /**
132     * Create a dictionary from passed data. This is intended for unit tests only.
133     * @param context the test context to create this data from.
134     * @param dictionary the file to read
135     * @param startOffset the offset in the file where the data starts
136     * @param length the length of the data
137     * @param useFullEditDistance whether to use the full edit distance in suggestions
138     * @return the created dictionary, or null.
139     */
140    public static Dictionary createDictionaryForTest(Context context, File dictionary,
141            long startOffset, long length, final boolean useFullEditDistance, Locale locale) {
142        if (dictionary.isFile()) {
143            return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
144                    useFullEditDistance, locale, Dictionary.TYPE_MAIN);
145        } else {
146            Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
147            return null;
148        }
149    }
150
151    /**
152     * Find out whether a dictionary is available for this locale.
153     * @param context the context on which to check resources.
154     * @param locale the locale to check for.
155     * @return whether a (non-placeholder) dictionary is available or not.
156     */
157    public static boolean isDictionaryAvailable(Context context, Locale locale) {
158        final Resources res = context.getResources();
159        return 0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
160    }
161
162    private static final String DEFAULT_MAIN_DICT = "main";
163    private static final String MAIN_DICT_PREFIX = "main_";
164
165    /**
166     * Helper method to return a dictionary res id for a locale, or 0 if none.
167     * @param locale dictionary locale
168     * @return main dictionary resource id
169     */
170    private static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
171            final Locale locale) {
172        int resId;
173        // Try to find main_language_country dictionary.
174        if (!locale.getCountry().isEmpty()) {
175            final String dictLanguageCountry = MAIN_DICT_PREFIX + locale.toString().toLowerCase();
176            if ((resId = res.getIdentifier(
177                    dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
178                return resId;
179            }
180        }
181
182        // Try to find main_language dictionary.
183        final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
184        if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
185            return resId;
186        }
187
188        // Not found, return 0
189        return 0;
190    }
191
192    /**
193     * Returns a main dictionary resource id
194     * @param locale dictionary locale
195     * @return main dictionary resource id
196     */
197    public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
198        int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
199        if (0 != resourceId) return resourceId;
200        return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
201    }
202}
203