Add different ways of reading the dictionary file.

This change adds basic support for an external dictionary provider.
It adds methods for reading the dictionary itself from an asset in
the dictionary provider package directly, obtaining the file name
through the ContentProvider interface; it also adds a way of getting
the data through an InputStream and copying the file locally.

Incidentally this change also adds the code needed to listen for
updating the dictionary provider package and reloading it in time.

This change also goes hand-in-hand with Iab31db6e, which implements
the small closed part of this.

Issue: 3414944

Change-Id: I5e4fff99a59bb99dbdb002102db6c90e6cb41c8a
main
Jean Chalard 2011-03-14 11:46:15 -07:00
parent 9807ab27ea
commit cba93f50c3
11 changed files with 497 additions and 16 deletions

View File

@ -0,0 +1,52 @@
/*
* 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 java.io.File;
/**
* Immutable class to hold the address of an asset.
* As opposed to a normal file, an asset is usually represented as a contiguous byte array in
* the package file. Open it correctly thus requires the name of the package it is in, but
* also the offset in the file and the length of this data. This class encapsulates these three.
*/
class AssetFileAddress {
public final String mFilename;
public final long mOffset;
public final long mLength;
public AssetFileAddress(final String filename, final long offset, final long length) {
mFilename = filename;
mOffset = offset;
mLength = length;
}
public static AssetFileAddress makeFromFileName(final String filename) {
if (null == filename) return null;
File f = new File(filename);
if (null == f || !f.isFile()) return null;
return new AssetFileAddress(filename, 0l, f.length());
}
public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
final long offset, final long length) {
if (null == filename) return null;
File f = new File(filename);
if (null == f || !f.isFile()) return null;
return new AssetFileAddress(filename, offset, length);
}
}

View File

@ -26,14 +26,18 @@ import android.util.Log;
import java.io.File;
import java.util.Arrays;
import java.util.Locale;
/**
* Implements a static, compacted, binary dictionary of standard words.
*/
public class BinaryDictionary extends Dictionary {
public static final String DICTIONARY_PACK_AUTHORITY =
"com.android.inputmethod.latin.dictionarypack";
/**
* There is difference between what java and native code can handle.
* There is a difference between what java and native code can handle.
* This value should only be used in BinaryDictionary.java
* It is necessary to keep it at this value because some languages e.g. German have
* really long words.
@ -85,10 +89,11 @@ public class BinaryDictionary extends Dictionary {
}
/**
* Initialize a dictionary from a raw resource file
* Initializes a dictionary from a raw resource file
* @param context application context for reading resources
* @param resId the resource containing the raw binary dictionary
* @return initialized instance of BinaryDictionary
* @param dicTypeId the type of the dictionary being created, out of the list in Suggest.DIC_*
* @return an initialized instance of BinaryDictionary
*/
public static BinaryDictionary initDictionary(Context context, int resId, int dicTypeId) {
synchronized (sInstance) {
@ -146,6 +151,37 @@ public class BinaryDictionary extends Dictionary {
Utils.loadNativeLibrary();
}
/**
* Initializes a dictionary from a dictionary pack.
*
* This searches for a content provider providing a dictionary pack for the specified
* locale. If none is found, it falls back to using the resource passed as fallBackResId
* as a dictionary.
* @param context application context for reading resources
* @param dicTypeId the type of the dictionary being created, out of the list in Suggest.DIC_*
* @param locale the locale for which to create the dictionary
* @param fallBackResId the id of the resource to use as a fallback if no pack is found
* @return an initialized instance of BinaryDictionary
*/
public static BinaryDictionary initDictionaryFromManager(Context context, int dicTypeId,
Locale locale, int fallbackResId) {
if (null == locale) {
Log.e(TAG, "No locale defined for dictionary");
return initDictionary(context, fallbackResId, dicTypeId);
}
synchronized (sInstance) {
sInstance.closeInternal();
final AssetFileAddress dictFile = BinaryDictionaryGetter.getDictionaryFile(locale,
context, fallbackResId);
if (null != dictFile) {
sInstance.loadDictionary(dictFile.mFilename, dictFile.mOffset, dictFile.mLength);
sInstance.mDicTypeId = dicTypeId;
}
}
return sInstance;
}
private native int openNative(String sourceDir, long dictOffset, long dictSize,
int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength,
int maxWords, int maxAlternatives);

View File

@ -0,0 +1,141 @@
/*
* 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.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
/**
* Group class for static methods to help with creation and getting of the binary dictionary
* file from the dictionary provider
*/
public class BinaryDictionaryFileDumper {
/**
* The size of the temporary buffer to copy files.
*/
static final int FILE_READ_BUFFER_SIZE = 1024;
// Prevents this class to be accidentally instantiated.
private BinaryDictionaryFileDumper() {
}
/**
* Generates a file name that matches the locale passed as an argument.
* The file name is basically the result of the .toString() method, except we replace
* any @File.separator with an underscore to avoid generating a file name that may not
* be created.
* @param locale the locale for which to get the file name
* @param context the context to use for getting the directory
* @return the name of the file to be created
*/
private static String getCacheFileNameForLocale(Locale locale, Context context) {
// The following assumes two things :
// 1. That File.separator is not the same character as "_"
// I don't think any android system will ever use "_" as a path separator
// 2. That no two locales differ by only a File.separator versus a "_"
// Since "_" can't be part of locale components this should be safe.
// Examples:
// en -> en
// en_US_POSIX -> en_US_POSIX
// en__foo/bar -> en__foo_bar
final String[] separator = { File.separator };
final String[] empty = { "_" };
final CharSequence basename = TextUtils.replace(locale.toString(), separator, empty);
return context.getFilesDir() + File.separator + basename;
}
/**
* Return for a given locale the provider URI to query to get the dictionary.
*/
public static Uri getProviderUri(Locale locale) {
return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
.authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath(
locale.toString()).build();
}
/**
* Queries a content provider for dictionary data for some locale and returns it as a file name.
*
* This will query a content provider for dictionary data for a given locale, and return
* the name of a file suitable to be mmap'ed. It will copy it to local storage if needed.
* It should also check the dictionary version to avoid unnecessary copies but this is
* still in TODO state.
* This will make the data from the content provider the cached dictionary for this locale,
* overwriting any previous cached data.
* @returns the name of the file, or null if no data could be obtained.
* @throw FileNotFoundException if the provider returns non-existent data.
* @throw IOException if the provider-returned data could not be read.
*/
public static String getDictionaryFileFromContentProvider(Locale locale, Context context)
throws FileNotFoundException, IOException {
// TODO: check whether the dictionary is the same or not and if it is, return the cached
// file.
final ContentResolver resolver = context.getContentResolver();
final Uri dictionaryPackUri = getProviderUri(locale);
final InputStream stream = resolver.openInputStream(dictionaryPackUri);
if (null == stream) return null;
return copyFileTo(stream, getCacheFileNameForLocale(locale, context));
}
/**
* Accepts a file as dictionary data for some locale and returns the name of a file.
*
* This will make the data in the input file the cached dictionary for this locale, overwriting
* any previous cached data.
*/
public static String getDictionaryFileFromFile(String fileName, Locale locale,
Context context) throws FileNotFoundException, IOException {
return copyFileTo(new FileInputStream(fileName), getCacheFileNameForLocale(locale,
context));
}
/**
* Accepts a resource number as dictionary data for some locale and returns the name of a file.
*
* This will make the resource the cached dictionary for this locale, overwriting any previous
* cached data.
*/
public static String getDictionaryFileFromResource(int resource, Locale locale,
Context context) throws FileNotFoundException, IOException {
return copyFileTo(context.getResources().openRawResource(resource),
getCacheFileNameForLocale(locale, context));
}
/**
* Copies the data in an input stream to a target file, creating the file if necessary and
* overwriting it if it already exists.
*/
private static String copyFileTo(final InputStream input, final String outputFileName)
throws FileNotFoundException, IOException {
final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
final FileOutputStream output = new FileOutputStream(outputFileName);
for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
output.write(buffer, 0, readBytes);
input.close();
return outputFileName;
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.Context;
import android.content.res.AssetFileDescriptor;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Locale;
/**
* Helper class to get the address of a mmap'able dictionary file.
*/
class BinaryDictionaryGetter {
/**
* Used for Log actions from this class
*/
private static final String TAG = BinaryDictionaryGetter.class.getSimpleName();
// Prevents this from being instantiated
private BinaryDictionaryGetter() {}
/**
* Returns a file address from a resource, or null if it cannot be opened.
*/
private static AssetFileAddress loadFallbackResource(Context context, int fallbackResId) {
final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId);
if (afd == null) {
Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId="
+ fallbackResId);
return null;
}
return AssetFileAddress.makeFromFileNameAndOffset(
context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
}
/**
* Returns a file address for a given locale, trying relevant methods in order.
*
* Tries to get a binary dictionary from various sources, in order:
* - Uses a private method of getting a private dictionary, as implemented by the
* PrivateBinaryDictionaryGetter class.
* If that fails:
* - Uses a content provider to get a public dictionary, as per the protocol described
* in BinaryDictionaryFileDumper.
* If that fails:
* - Gets a file name from the fallback resource passed as an argument.
* If that fails:
* - Returns null.
* @return The address of a valid file, or null.
* @throws FileNotFoundException if a dictionary provider returned a file name, but the
* file cannot be found.
* @throws IOException if there was an I/O problem reading or copying a file.
*/
public static AssetFileAddress getDictionaryFile(Locale locale, Context context,
int fallbackResId) {
// Try first to query a private file signed the same way.
final AssetFileAddress privateFile =
PrivateBinaryDictionaryGetter.getDictionaryFile(locale, context);
if (null != privateFile) {
return privateFile;
} else {
try {
// If that was no-go, try to find a publicly exported dictionary.
final String fileName = BinaryDictionaryFileDumper.
getDictionaryFileFromContentProvider(locale, context);
return AssetFileAddress.makeFromFileName(fileName);
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to create dictionary file from provider for locale "
+ locale.toString() + ": falling back to internal dictionary");
return loadFallbackResource(context, fallbackResId);
} catch (IOException e) {
Log.e(TAG, "Unable to read source data for locale "
+ locale.toString() + ": falling back to internal dictionary");
return loadFallbackResource(context, fallbackResId);
}
}
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
/**
* Takes action to reload the necessary data when a dictionary pack was added/removed.
*/
public class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver {
final LatinIME mService;
public DictionaryPackInstallBroadcastReceiver(final LatinIME service) {
mService = service;
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final PackageManager manager = context.getPackageManager();
// We need to reread the dictionary if a new dictionary package is installed.
if (action.equals(Intent.ACTION_PACKAGE_ADDED)) {
final Uri packageUri = intent.getData();
if (null == packageUri) return; // No package name : we can't do anything
final String packageName = packageUri.getSchemeSpecificPart();
if (null == packageName) return;
final PackageInfo packageInfo;
try {
packageInfo = manager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS);
} catch (android.content.pm.PackageManager.NameNotFoundException e) {
return; // No package info : we can't do anything
}
final ProviderInfo[] providers = packageInfo.providers;
if (null == providers) return; // No providers : it is not a dictionary.
// Search for some dictionary pack in the just-installed package. If found, reread.
boolean found = false;
for (ProviderInfo info : providers) {
if (BinaryDictionary.DICTIONARY_PACK_AUTHORITY.equals(info.authority)) {
mService.resetSuggestMainDict();
return;
}
}
// If we come here none of the authorities matched the one we searched for.
// We can exit safely.
return;
} else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
&& !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
// When the dictionary package is removed, we need to reread dictionary (to use the
// next-priority one, or stop using a dictionary at all if this was the only one,
// since this is the user request).
// If we are replacing the package, we will receive ADDED right away so no need to
// remove the dictionary at the moment, since we will do it when we receive the
// ADDED broadcast.
// TODO: Only reload dictionary on REMOVED when the removed package is the one we
// read dictionary from?
mService.resetSuggestMainDict();
}
}
}

View File

@ -106,8 +106,8 @@ public class InputLanguageSelection extends PreferenceActivity {
conf.locale = locale;
res.updateConfiguration(conf, res.getDisplayMetrics());
int mainDicResId = Utils.getMainDictionaryResourceId(res);
BinaryDictionary bd = BinaryDictionary.initDictionary(this, mainDicResId, Suggest.DIC_MAIN);
BinaryDictionary bd = BinaryDictionary.initDictionaryFromManager(this, Suggest.DIC_MAIN,
locale, Utils.getMainDictionaryResourceId(res));
// Is the dictionary larger than a placeholder? Arbitrarily chose a lower limit of
// 4000-5000 words, whereas the LARGE_DICTIONARY is about 20000+ words.

View File

@ -119,6 +119,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Key events coming any faster than this are long-presses.
private static final int QUICK_PRESS = 200;
/**
* The name of the scheme used by the Package Manager to warn of a new package installation,
* replacement or removal.
*/
private static final String SCHEME_PACKAGE = "package";
private int mSuggestionVisibility;
private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
= R.string.prefs_suggestion_visibility_show_value;
@ -207,6 +213,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: Move this flag to VoiceIMEConnector
private boolean mConfigurationChanging;
// Object for reacting to adding/removing a dictionary pack.
private BroadcastReceiver mDictionaryPackInstallReceiver =
new DictionaryPackInstallBroadcastReceiver(this);
// Keeps track of most recently inserted text (multi-character key) for reverting
private CharSequence mEnteredText;
@ -414,18 +424,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mOrientation = res.getConfiguration().orientation;
initSuggestPuncList();
// register to receive ringer mode change and network state change.
// Register to receive ringer mode change and network state change.
// Also receive installation and removal of a dictionary pack.
final IntentFilter filter = new IntentFilter();
filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(mReceiver, filter);
mVoiceConnector = VoiceConnector.init(this, prefs, mHandler);
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
packageFilter.addDataScheme(SCHEME_PACKAGE);
registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
}
private void initSuggest() {
String locale = mSubtypeSwitcher.getInputLocaleStr();
final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
final Locale keyboardLocale = new Locale(localeStr);
Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(new Locale(locale));
final Locale savedLocale = mSubtypeSwitcher.changeSystemLocale(keyboardLocale);
if (mSuggest != null) {
mSuggest.close();
}
@ -434,20 +452,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Resources res = mResources;
int mainDicResId = Utils.getMainDictionaryResourceId(res);
mSuggest = new Suggest(this, mainDicResId);
mSuggest = new Suggest(this, mainDicResId, keyboardLocale);
loadAndSetAutoCorrectionThreshold(prefs);
updateAutoTextEnabled();
mUserDictionary = new UserDictionary(this, locale);
mUserDictionary = new UserDictionary(this, localeStr);
mSuggest.setUserDictionary(mUserDictionary);
mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
mSuggest.setContactsDictionary(mContactsDictionary);
mAutoDictionary = new AutoDictionary(this, this, locale, Suggest.DIC_AUTO);
mAutoDictionary = new AutoDictionary(this, this, localeStr, Suggest.DIC_AUTO);
mSuggest.setAutoDictionary(mAutoDictionary);
mUserBigramDictionary = new UserBigramDictionary(this, this, locale, Suggest.DIC_USER);
mUserBigramDictionary = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER);
mSuggest.setUserBigramDictionary(mUserBigramDictionary);
updateCorrectionMode();
@ -457,6 +475,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSubtypeSwitcher.changeSystemLocale(savedLocale);
}
/* package private */ void resetSuggestMainDict() {
final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
final Locale keyboardLocale = new Locale(localeStr);
int mainDicResId = Utils.getMainDictionaryResourceId(mResources);
mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
}
@Override
public void onDestroy() {
if (mSuggest != null) {
@ -464,6 +489,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mSuggest = null;
}
unregisterReceiver(mReceiver);
unregisterReceiver(mDictionaryPackInstallReceiver);
mVoiceConnector.destroy();
LatinImeLogger.commit();
LatinImeLogger.onDestroy();

View File

@ -0,0 +1,28 @@
/*
* 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.Context;
import java.util.Locale;
class PrivateBinaryDictionaryGetter {
private PrivateBinaryDictionaryGetter() {}
public static AssetFileAddress getDictionaryFile(Locale locale, Context context) {
return null;
}
}

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@ -106,8 +107,9 @@ public class Suggest implements Dictionary.WordCallback {
private int mCorrectionMode = CORRECTION_BASIC;
public Suggest(Context context, int dictionaryResId) {
init(context, BinaryDictionary.initDictionary(context, dictionaryResId, DIC_MAIN));
public Suggest(Context context, int dictionaryResId, Locale locale) {
init(context, BinaryDictionary.initDictionaryFromManager(context, DIC_MAIN, locale,
dictionaryResId));
}
/* package for test */ Suggest(File dictionary, long startOffset, long length,
@ -130,6 +132,19 @@ public class Suggest implements Dictionary.WordCallback {
initPool();
}
public void resetMainDict(Context context, int dictionaryResId, Locale locale) {
final BinaryDictionary newMainDict = BinaryDictionary.initDictionaryFromManager(context,
DIC_MAIN, locale, dictionaryResId);
mMainDict = newMainDict;
if (null == newMainDict) {
mUnigramDictionaries.remove(DICT_KEY_MAIN);
mBigramDictionaries.remove(DICT_KEY_MAIN);
} else {
mUnigramDictionaries.put(DICT_KEY_MAIN, newMainDict);
mBigramDictionaries.put(DICT_KEY_MAIN, newMainDict);
}
}
private void initPool() {
for (int i = 0; i < mPrefMaxSuggestions; i++) {
StringBuilder sb = new StringBuilder(getApproxMaxWordLength());

View File

@ -551,7 +551,9 @@ public class Utils {
* @return main dictionary resource id
*/
public static int getMainDictionaryResourceId(Resources res) {
return res.getIdentifier("main", "raw", LatinIME.class.getPackage().getName());
final String MAIN_DIC_NAME = "main";
String packageName = LatinIME.class.getPackage().getName();
return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName);
}
public static void loadNativeLibrary() {

View File

@ -34,7 +34,9 @@ public class SuggestHelper {
private final KeyDetector mKeyDetector;
public SuggestHelper(Context context, int dictionaryId, KeyboardId keyboardId) {
mSuggest = new Suggest(context, dictionaryId);
// Use null as the locale for Suggest so as to force it to use the internal dictionary
// (and not try to find a dictionary provider for a specified locale)
mSuggest = new Suggest(context, dictionaryId, null);
mKeyboard = new LatinKeyboard(context, keyboardId);
mKeyDetector = new ProximityKeyDetector();
init();