aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/DictionaryFactory.java
blob: 49608d8309bc32efa14666bb96e2c00903092ba8 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.latin;

import android.content.ContentProviderClient;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.util.Log;

import com.android.inputmethod.latin.utils.DictionaryInfoUtils;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Locale;

/**
 * Factory for dictionary instances.
 */
public final class DictionaryFactory {
    private static final String TAG = DictionaryFactory.class.getSimpleName();

    /**
     * Initializes a main dictionary collection from a dictionary pack, with explicit flags.
     *
     * This searches for a content provider providing a dictionary pack for the specified
     * locale. If none is found, it falls back to the built-in dictionary - if any.
     * @param context application context for reading resources
     * @param locale the locale for which to create the dictionary
     * @return an initialized instance of DictionaryCollection
     */
    public static DictionaryCollection createMainDictionaryFromManager(final Context context,
            final Locale locale) {
        if (null == locale) {
            Log.e(TAG, "No locale defined for dictionary");
            return new DictionaryCollection(Dictionary.TYPE_MAIN, locale,
                    createReadOnlyBinaryDictionary(context, locale));
        }

        final LinkedList<Dictionary> dictList = new LinkedList<>();
        final ArrayList<AssetFileAddress> assetFileList =
                BinaryDictionaryGetter.getDictionaryFiles(locale, context);
        if (null != assetFileList) {
            for (final AssetFileAddress f : assetFileList) {
                final ReadOnlyBinaryDictionary readOnlyBinaryDictionary =
                        new ReadOnlyBinaryDictionary(f.mFilename, f.mOffset, f.mLength,
                                false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
                if (readOnlyBinaryDictionary.isValidDictionary()) {
                    dictList.add(readOnlyBinaryDictionary);
                } else {
                    readOnlyBinaryDictionary.close();
                    // Prevent this dictionary to do any further harm.
                    killDictionary(context, f);
                }
            }
        }

        // If the list is empty, that means we should not use any dictionary (for example, the user
        // explicitly disabled the main dictionary), so the following is okay. dictList is never
        // null, but if for some reason it is, DictionaryCollection handles it gracefully.
        return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList);
    }

    /**
     * Kills a dictionary so that it is never used again, if possible.
     * @param context The context to contact the dictionary provider, if possible.
     * @param f A file address to the dictionary to kill.
     */
    public static void killDictionary(final Context context, final AssetFileAddress f) {
        if (f.pointsToPhysicalFile()) {
            f.deleteUnderlyingFile();
            // Warn the dictionary provider if the dictionary came from there.
            final ContentProviderClient providerClient;
            try {
                providerClient = context.getContentResolver().acquireContentProviderClient(
                        BinaryDictionaryFileDumper.getProviderUriBuilder("").build());
            } catch (final SecurityException e) {
                Log.e(TAG, "No permission to communicate with the dictionary provider", e);
                return;
            }
            if (null == providerClient) {
                Log.e(TAG, "Can't establish communication with the dictionary provider");
                return;
            }
            final String wordlistId =
                    DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName());
            // TODO: this is a reasonable last resort, but it is suboptimal.
            // The following will remove the entry for this dictionary with the dictionary
            // provider. When the metadata is downloaded again, we will try downloading it
            // again.
            // However, in the practice that will mean the user will find themselves without
            // the new dictionary. That's fine for languages where it's included in the APK,
            // but for other languages it will leave the user without a dictionary at all until
            // the next update, which may be a few days away.
            // Ideally, we would trigger a new download right away, and use increasing retry
            // delays for this particular id/version combination.
            // Then again, this is expected to only ever happen in case of human mistake. If
            // the wrong file is on the server, the following is still doing the right thing.
            // If it's a file left over from the last version however, it's not great.
            BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider(
                    providerClient,
                    context.getString(R.string.dictionary_pack_client_id),
                    wordlistId);
        }
    }

    /**
     * Initializes a read-only binary dictionary from a raw resource file
     * @param context application context for reading resources
     * @param locale the locale to use for the resource
     * @return an initialized instance of ReadOnlyBinaryDictionary
     */
    private static ReadOnlyBinaryDictionary createReadOnlyBinaryDictionary(final Context context,
            final Locale locale) {
        AssetFileDescriptor afd = null;
        try {
            final int resId = DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
                    context.getResources(), locale);
            if (0 == resId) return null;
            afd = context.getResources().openRawResourceFd(resId);
            if (afd == null) {
                Log.e(TAG, "Found the resource but it is compressed. resId=" + resId);
                return null;
            }
            final String sourceDir = context.getApplicationInfo().sourceDir;
            final File packagePath = new File(sourceDir);
            // TODO: Come up with a way to handle a directory.
            if (!packagePath.isFile()) {
                Log.e(TAG, "sourceDir is not a file: " + sourceDir);
                return null;
            }
            return new ReadOnlyBinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(),
                    false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
        } catch (android.content.res.Resources.NotFoundException e) {
            Log.e(TAG, "Could not find the resource");
            return null;
        } finally {
            if (null != afd) {
                try {
                    afd.close();
                } catch (java.io.IOException e) {
                    /* IOException on close ? What am I supposed to do ? */
                }
            }
        }
    }
}