Merge "Add different ways of reading the dictionary file."

main
Jean Chalard 2011-03-18 12:19:00 -07:00 committed by Android (Google) Code Review
commit 0a7cf81ca2
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

@ -120,6 +120,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;
@ -208,6 +214,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;
@ -415,18 +425,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();
}
@ -435,20 +453,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();
@ -458,6 +476,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) {
@ -465,6 +490,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();