commit 923bf41f853a544fd0d71fbf7dc90359ec359812 Author: The Android Open Source Project Date: Fri Mar 13 15:11:42 2009 -0700 auto import from //branches/cupcake/...@138744 diff --git a/Android.mk b/Android.mk new file mode 100755 index 000000000..e3215e822 --- /dev/null +++ b/Android.mk @@ -0,0 +1,17 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_PACKAGE_NAME := LatinIME + +LOCAL_CERTIFICATE := shared + +LOCAL_JNI_SHARED_LIBRARIES := libjni_latinime + +LOCAL_AAPT_FLAGS := -0 .dict + +include $(BUILD_PACKAGE) +include $(LOCAL_PATH)/dictionary/Android.mk diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100755 index 000000000..89cdad2ca --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/dictionaries/sample.xml b/dictionaries/sample.xml new file mode 100644 index 000000000..85233b63a --- /dev/null +++ b/dictionaries/sample.xml @@ -0,0 +1,16 @@ + + + this + is + sample + wordlist + + diff --git a/dictionary/Android.mk b/dictionary/Android.mk new file mode 100644 index 000000000..9ba9f75ec --- /dev/null +++ b/dictionary/Android.mk @@ -0,0 +1,28 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_C_INCLUDES += $(LOCAL_PATH)/src + +LOCAL_SRC_FILES := \ + jni/com_android_inputmethod_latin_BinaryDictionary.cpp \ + src/dictionary.cpp + +LOCAL_C_INCLUDES += \ + external/icu4c/common \ + $(JNI_H_INCLUDE) + +LOCAL_LDLIBS := -lm + +LOCAL_PRELINK_MODULE := false + +LOCAL_SHARED_LIBRARIES := \ + libandroid_runtime \ + libcutils \ + libutils \ + libicuuc + +LOCAL_MODULE := libjni_latinime + +LOCAL_MODULE_TAGS := user + +include $(BUILD_SHARED_LIBRARY) diff --git a/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp new file mode 100644 index 000000000..65c640b40 --- /dev/null +++ b/dictionary/jni/com_android_inputmethod_latin_BinaryDictionary.cpp @@ -0,0 +1,207 @@ +/* +** +** Copyright 2009, 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. +*/ + +#define LOG_TAG "BinaryDictionary" +#include "utils/Log.h" + +#include +#include +#include +#include + +#include +#include "utils/AssetManager.h" +#include "utils/Asset.h" + +#include "dictionary.h" + +// ---------------------------------------------------------------------------- + +using namespace latinime; + +using namespace android; + +static jfieldID sDescriptorField; +static jfieldID sAssetManagerNativeField; +static jmethodID sAddWordMethod; + +// +// helper function to throw an exception +// +static void throwException(JNIEnv *env, const char* ex, const char* fmt, int data) +{ + if (jclass cls = env->FindClass(ex)) { + char msg[1000]; + sprintf(msg, fmt, data); + env->ThrowNew(cls, msg); + env->DeleteLocalRef(cls); + } +} + +static jint latinime_BinaryDictionary_open + (JNIEnv *env, jobject object, jobject assetManager, jstring resourceString, + jint typedLetterMultiplier, jint fullWordMultiplier) +{ + // Get the native file descriptor from the FileDescriptor object + AssetManager *am = (AssetManager*) env->GetIntField(assetManager, sAssetManagerNativeField); + if (!am) { + LOGE("DICT: Couldn't get AssetManager native peer\n"); + return 0; + } + const char *resourcePath = env->GetStringUTFChars(resourceString, NULL); + + Asset *dictAsset = am->openNonAsset(resourcePath, Asset::ACCESS_BUFFER); + if (dictAsset == NULL) { + LOGE("DICT: Couldn't get asset %s\n", resourcePath); + env->ReleaseStringUTFChars(resourceString, resourcePath); + return 0; + } + + void *dict = (void*) dictAsset->getBuffer(false); + if (dict == NULL) { + LOGE("DICT: Dictionary buffer is null\n"); + env->ReleaseStringUTFChars(resourceString, resourcePath); + return 0; + } + Dictionary *dictionary = new Dictionary(dict, typedLetterMultiplier, fullWordMultiplier); + dictionary->setAsset(dictAsset); + + env->ReleaseStringUTFChars(resourceString, resourcePath); + return (jint) dictionary; +} + +static int latinime_BinaryDictionary_getSuggestions( + JNIEnv *env, jobject object, jint dict, jintArray inputArray, jint arraySize, + jcharArray outputArray, jintArray frequencyArray, jint maxWordLength, jint maxWords, + jint maxAlternatives) +{ + Dictionary *dictionary = (Dictionary*) dict; + if (dictionary == NULL) + return 0; + + int *frequencies = env->GetIntArrayElements(frequencyArray, NULL); + int *inputCodes = env->GetIntArrayElements(inputArray, NULL); + jchar *outputChars = env->GetCharArrayElements(outputArray, NULL); + + int count = dictionary->getSuggestions(inputCodes, arraySize, (unsigned short*) outputChars, frequencies, + maxWordLength, maxWords, maxAlternatives); + + env->ReleaseIntArrayElements(frequencyArray, frequencies, JNI_COMMIT); + env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT); + env->ReleaseCharArrayElements(outputArray, outputChars, JNI_COMMIT); + + return count; +} + +static jboolean latinime_BinaryDictionary_isValidWord + (JNIEnv *env, jobject object, jint dict, jcharArray wordArray, jint wordLength) +{ + Dictionary *dictionary = (Dictionary*) dict; + if (dictionary == NULL) return (jboolean) false; + + jchar *word = env->GetCharArrayElements(wordArray, NULL); + jboolean result = dictionary->isValidWord((unsigned short*) word, wordLength); + env->ReleaseCharArrayElements(wordArray, word, JNI_ABORT); + + return result; +} + +static void latinime_BinaryDictionary_close + (JNIEnv *env, jobject object, jint dict) +{ + Dictionary *dictionary = (Dictionary*) dict; + ((Asset*) dictionary->getAsset())->close(); + delete (Dictionary*) dict; +} + +// ---------------------------------------------------------------------------- + +static JNINativeMethod gMethods[] = { + {"openNative", "(Landroid/content/res/AssetManager;Ljava/lang/String;II)I", + (void*)latinime_BinaryDictionary_open}, + {"closeNative", "(I)V", (void*)latinime_BinaryDictionary_close}, + {"getSuggestionsNative", "(I[II[C[IIII)I", (void*)latinime_BinaryDictionary_getSuggestions}, + {"isValidWordNative", "(I[CI)Z", (void*)latinime_BinaryDictionary_isValidWord} +}; + +static int registerNativeMethods(JNIEnv* env, const char* className, + JNINativeMethod* gMethods, int numMethods) +{ + jclass clazz; + + clazz = env->FindClass(className); + if (clazz == NULL) { + fprintf(stderr, + "Native registration unable to find class '%s'\n", className); + return JNI_FALSE; + } + if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { + fprintf(stderr, "RegisterNatives failed for '%s'\n", className); + return JNI_FALSE; + } + + return JNI_TRUE; +} + +static int registerNatives(JNIEnv *env) +{ + const char* const kClassPathName = "com/android/inputmethod/latin/BinaryDictionary"; + jclass clazz; + + clazz = env->FindClass("java/io/FileDescriptor"); + if (clazz == NULL) { + LOGE("Can't find %s", "java/io/FileDescriptor"); + return -1; + } + sDescriptorField = env->GetFieldID(clazz, "descriptor", "I"); + + clazz = env->FindClass("android/content/res/AssetManager"); + if (clazz == NULL) { + LOGE("Can't find %s", "java/io/FileDescriptor"); + return -1; + } + sAssetManagerNativeField = env->GetFieldID(clazz, "mObject", "I"); + + return registerNativeMethods(env, + kClassPathName, gMethods, sizeof(gMethods) / sizeof(gMethods[0])); +} + +/* + * Returns the JNI version on success, -1 on failure. + */ +jint JNI_OnLoad(JavaVM* vm, void* reserved) +{ + JNIEnv* env = NULL; + jint result = -1; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { + fprintf(stderr, "ERROR: GetEnv failed\n"); + goto bail; + } + assert(env != NULL); + + if (!registerNatives(env)) { + fprintf(stderr, "ERROR: BinaryDictionary native registration failed\n"); + goto bail; + } + + /* success -- return valid version number */ + result = JNI_VERSION_1_4; + +bail: + return result; +} diff --git a/dictionary/src/basechars.h b/dictionary/src/basechars.h new file mode 100644 index 000000000..5a4406606 --- /dev/null +++ b/dictionary/src/basechars.h @@ -0,0 +1,172 @@ +/** + * Table mapping most combined Latin, Greek, and Cyrillic characters + * to their base characters. If c is in range, BASE_CHARS[c] == c + * if c is not a combined character, or the base character if it + * is combined. + */ +static unsigned short BASE_CHARS[] = { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, + 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f, + 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, + 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f, + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, + 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, + 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, + 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, + 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, + 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f, + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, + 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, + 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f, + 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7, + 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020, + 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7, + 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf, + 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043, + 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049, + 0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7, + 0x004f, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00de, 0x0073, // Manually changed d8 to 4f + // Manually changed df to 73 + 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063, + 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, + 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7, + 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, // Manually changed f8 to 6f + 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063, + 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064, + 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065, + 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067, + 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127, + 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, + 0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b, + 0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, + 0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e, + 0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f, + 0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072, + 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073, + 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167, + 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, + 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079, + 0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073, + 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187, + 0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f, + 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197, + 0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f, + 0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7, + 0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055, + 0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7, + 0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf, + 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c, + 0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049, + 0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc, + 0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4, + 0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067, + 0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292, + 0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7, + 0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8, + 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065, + 0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f, + 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075, + 0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068, + 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061, + 0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f, + 0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237, + 0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f, + 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247, + 0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f, + 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257, + 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f, + 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267, + 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f, + 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277, + 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f, + 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287, + 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f, + 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, + 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f, + 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7, + 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af, + 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077, + 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf, + 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7, + 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf, + 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7, + 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df, + 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7, + 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef, + 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7, + 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff, + 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, + 0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f, + 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317, + 0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f, + 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327, + 0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f, + 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337, + 0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f, + 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347, + 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f, + 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357, + 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f, + 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, + 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f, + 0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377, + 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f, + 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7, + 0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9, + 0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, + 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f, + 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7, + 0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9, + 0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7, + 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf, + 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7, + 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf, + 0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7, + 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df, + 0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7, + 0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef, + 0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7, + 0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff, + 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406, + 0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, + 0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, + 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, + 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, + 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, + 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456, + 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f, + 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467, + 0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f, + 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475, + 0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f, + 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, + 0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f, + 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497, + 0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f, + 0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7, + 0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af, + 0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7, + 0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf, + 0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7, + 0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf, + 0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435, + 0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437, + 0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e, + 0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443, + 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7, + 0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff, +}; + +// generated with: +// cat UnicodeData.txt | perl -e 'while (<>) { @foo = split(/;/); $foo[5] =~ s/<.*> //; $base[hex($foo[0])] = hex($foo[5]);} for ($i = 0; $i < 0x500; $i += 8) { for ($j = $i; $j < $i + 8; $j++) { printf("0x%04x, ", $base[$j] ? $base[$j] : $j)}; print "\n"; }' diff --git a/dictionary/src/dictionary.cpp b/dictionary/src/dictionary.cpp new file mode 100644 index 000000000..6aecb6374 --- /dev/null +++ b/dictionary/src/dictionary.cpp @@ -0,0 +1,277 @@ +/* +** +** Copyright 2009, 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. +*/ + +#include +#include +#include +#include +#include + +#include + +//#define USE_ASSET_MANAGER + +#ifdef USE_ASSET_MANAGER +#include +#include +#endif + +#include "dictionary.h" +#include "basechars.h" + +#define DEBUG_DICT 0 + +namespace latinime { + +Dictionary::Dictionary(void *dict, int typedLetterMultiplier, int fullWordMultiplier) +{ + mDict = (unsigned char*) dict; + mTypedLetterMultiplier = typedLetterMultiplier; + mFullWordMultiplier = fullWordMultiplier; +} + +Dictionary::~Dictionary() +{ +} + +int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies, + int maxWordLength, int maxWords, int maxAlternatives) +{ + memset(frequencies, 0, maxWords * sizeof(*frequencies)); + memset(outWords, 0, maxWords * maxWordLength * sizeof(*outWords)); + + mFrequencies = frequencies; + mOutputChars = outWords; + mInputCodes = codes; + mInputLength = codesSize; + mMaxAlternatives = maxAlternatives; + mMaxWordLength = maxWordLength; + mMaxWords = maxWords; + mWords = 0; + + getWordsRec(0, 0, mInputLength * 3, false, 1, 0); + + if (DEBUG_DICT) LOGI("Returning %d words", mWords); + return mWords; +} + +unsigned short +Dictionary::getChar(int *pos) +{ + unsigned short ch = (unsigned short) (mDict[(*pos)++] & 0xFF); + // If the code is 255, then actual 16 bit code follows (in big endian) + if (ch == 0xFF) { + ch = ((mDict[*pos] & 0xFF) << 8) | (mDict[*pos + 1] & 0xFF); + (*pos) += 2; + } + return ch; +} + +int +Dictionary::getAddress(int *pos) +{ + int address = 0; + address += (mDict[*pos] & 0x7F) << 16; + address += (mDict[*pos + 1] & 0xFF) << 8; + address += (mDict[*pos + 2] & 0xFF); + *pos += 3; + return address; +} + +int +Dictionary::wideStrLen(unsigned short *str) +{ + if (!str) return 0; + unsigned short *end = str; + while (*end) + end++; + return end - str; +} + +bool +Dictionary::addWord(unsigned short *word, int length, int frequency) +{ + word[length] = 0; + if (DEBUG_DICT) LOGI("Found word = %s, freq = %d : \n", word, frequency); + + // Find the right insertion point + int insertAt = 0; + while (insertAt < mMaxWords) { + if (frequency > mFrequencies[insertAt] + || (mFrequencies[insertAt] == frequency + && length < wideStrLen(mOutputChars + insertAt * mMaxWordLength))) { + break; + } + insertAt++; + } + if (insertAt < mMaxWords) { + memmove((char*) mFrequencies + (insertAt + 1) * sizeof(mFrequencies[0]), + (char*) mFrequencies + insertAt * sizeof(mFrequencies[0]), + (mMaxWords - insertAt - 1) * sizeof(mFrequencies[0])); + mFrequencies[insertAt] = frequency; + memmove((char*) mOutputChars + (insertAt + 1) * mMaxWordLength * sizeof(short), + (char*) mOutputChars + (insertAt ) * mMaxWordLength * sizeof(short), + (mMaxWords - insertAt - 1) * sizeof(short) * mMaxWordLength); + unsigned short *dest = mOutputChars + (insertAt ) * mMaxWordLength; + while (length--) { + *dest++ = *word++; + } + *dest = 0; // NULL terminate + // Update the word count + if (insertAt + 1 > mWords) mWords = insertAt + 1; + if (DEBUG_DICT) LOGI("Added word at %d\n", insertAt); + return true; + } + return false; +} + +unsigned short +Dictionary::toLowerCase(unsigned short c, const int depth) { + if (c < sizeof(BASE_CHARS) / sizeof(BASE_CHARS[0])) { + c = BASE_CHARS[c]; + } + if (depth == 0) { + if (c >='A' && c <= 'Z') { + c |= 32; + } else if (c > 127) { + c = u_tolower(c); + } + } + return c; +} + +bool +Dictionary::sameAsTyped(unsigned short *word, int length) +{ + if (length != mInputLength) { + return false; + } + int *inputCodes = mInputCodes; + while (length--) { + if ((unsigned int) *inputCodes != (unsigned int) *word) { + return false; + } + inputCodes += mMaxAlternatives; + word++; + } + return true; +} + +static char QUOTE = '\''; + +void +Dictionary::getWordsRec(int pos, int depth, int maxDepth, bool completion, int snr, int inputIndex) +{ + // Optimization: Prune out words that are too long compared to how much was typed. + if (depth > maxDepth) { + return; + } + int count = getCount(&pos); + int *currentChars = NULL; + if (mInputLength <= inputIndex) { + completion = true; + } else { + currentChars = mInputCodes + (inputIndex * mMaxAlternatives); + } + + for (int i = 0; i < count; i++) { + unsigned short c = getChar(&pos); + unsigned short lowerC = toLowerCase(c, depth); + bool terminal = getTerminal(&pos); + int childrenAddress = getAddress(&pos); + int freq = getFreq(&pos); + // If we are only doing completions, no need to look at the typed characters. + if (completion) { + mWord[depth] = c; + if (terminal) { + addWord(mWord, depth + 1, freq * snr); + } + if (childrenAddress != 0) { + getWordsRec(childrenAddress, depth + 1, maxDepth, + completion, snr, inputIndex); + } + } else if (c == QUOTE && currentChars[0] != QUOTE) { + // Skip the ' and continue deeper + mWord[depth] = QUOTE; + if (childrenAddress != 0) { + getWordsRec(childrenAddress, depth + 1, maxDepth, false, snr, inputIndex); + } + } else { + int j = 0; + while (currentChars[j] > 0) { + int addedWeight = j == 0 ? mTypedLetterMultiplier : 1; + if (currentChars[j] == lowerC || currentChars[j] == c) { + mWord[depth] = c; + if (mInputLength == inputIndex + 1) { + if (terminal) { + if (//INCLUDE_TYPED_WORD_IF_VALID || + !sameAsTyped(mWord, depth + 1)) { + addWord(mWord, depth + 1, + (freq * snr * addedWeight * mFullWordMultiplier)); + } + } + if (childrenAddress != 0) { + getWordsRec(childrenAddress, depth + 1, + maxDepth, true, snr * addedWeight, inputIndex + 1); + } + } else if (childrenAddress != 0) { + getWordsRec(childrenAddress, depth + 1, maxDepth, + false, snr * addedWeight, inputIndex + 1); + } + } + j++; + } + } + } +} + +bool +Dictionary::isValidWord(unsigned short *word, int length) +{ + return isValidWordRec(0, word, 0, length); +} + +bool +Dictionary::isValidWordRec(int pos, unsigned short *word, int offset, int length) { + int count = getCount(&pos); + unsigned short currentChar = (unsigned short) word[offset]; + for (int j = 0; j < count; j++) { + unsigned short c = getChar(&pos); + int terminal = getTerminal(&pos); + int childPos = getAddress(&pos); + if (c == currentChar) { + if (offset == length - 1) { + if (terminal) { + return true; + } + } else { + if (childPos != 0) { + if (isValidWordRec(childPos, word, offset + 1, length)) { + return true; + } + } + } + } + getFreq(&pos); + // There could be two instances of each alphabet - upper and lower case. So continue + // looking ... + } + return false; +} + + +} // namespace latinime diff --git a/dictionary/src/dictionary.h b/dictionary/src/dictionary.h new file mode 100644 index 000000000..8574e0736 --- /dev/null +++ b/dictionary/src/dictionary.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009 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. + */ + +#ifndef LATINIME_DICTIONARY_H +#define LATINIME_DICTIONARY_H + +namespace latinime { + +class Dictionary { +public: + Dictionary(void *dict, int typedLetterMultipler, int fullWordMultiplier); + int getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies, + int maxWordLength, int maxWords, int maxAlternatives); + bool isValidWord(unsigned short *word, int length); + void setAsset(void *asset) { mAsset = asset; } + void *getAsset() { return mAsset; } + ~Dictionary(); + +private: + + int getAddress(int *pos); + bool getTerminal(int *pos) { return (mDict[*pos] & 0x80) > 0; } + int getFreq(int *pos) { return mDict[(*pos)++] & 0xFF; } + int getCount(int *pos) { return mDict[(*pos)++] & 0xFF; } + unsigned short getChar(int *pos); + int wideStrLen(unsigned short *str); + + bool sameAsTyped(unsigned short *word, int length); + bool addWord(unsigned short *word, int length, int frequency); + unsigned short toLowerCase(unsigned short c, int depth); + void getWordsRec(int pos, int depth, int maxDepth, bool completion, int frequency, + int inputIndex); + bool isValidWordRec(int pos, unsigned short *word, int offset, int length); + + unsigned char *mDict; + void *mAsset; + + int *mFrequencies; + int mMaxWords; + int mMaxWordLength; + int mWords; + unsigned short *mOutputChars; + int *mInputCodes; + int mInputLength; + int mMaxAlternatives; + unsigned short mWord[128]; + + int mFullWordMultiplier; + int mTypedLetterMultiplier; +}; + +// ---------------------------------------------------------------------------- + +}; // namespace latinime + +#endif // LATINIME_DICTIONARY_H diff --git a/res/drawable-land/keyboard_suggest_strip_divider.png b/res/drawable-land/keyboard_suggest_strip_divider.png new file mode 100644 index 000000000..e54c5b099 Binary files /dev/null and b/res/drawable-land/keyboard_suggest_strip_divider.png differ diff --git a/res/drawable/candidate_feedback_background.9.png b/res/drawable/candidate_feedback_background.9.png new file mode 100644 index 000000000..2a80f096d Binary files /dev/null and b/res/drawable/candidate_feedback_background.9.png differ diff --git a/res/drawable/dialog_bubble_step02.9.png b/res/drawable/dialog_bubble_step02.9.png new file mode 100755 index 000000000..d77f85fe2 Binary files /dev/null and b/res/drawable/dialog_bubble_step02.9.png differ diff --git a/res/drawable/dialog_bubble_step03.9.png b/res/drawable/dialog_bubble_step03.9.png new file mode 100755 index 000000000..16b4d0226 Binary files /dev/null and b/res/drawable/dialog_bubble_step03.9.png differ diff --git a/res/drawable/dialog_bubble_step04.9.png b/res/drawable/dialog_bubble_step04.9.png new file mode 100755 index 000000000..a24012d7c Binary files /dev/null and b/res/drawable/dialog_bubble_step04.9.png differ diff --git a/res/drawable/highlight_pressed.png b/res/drawable/highlight_pressed.png new file mode 100644 index 000000000..d27f1061a Binary files /dev/null and b/res/drawable/highlight_pressed.png differ diff --git a/res/drawable/ic_dialog_keyboard.png b/res/drawable/ic_dialog_keyboard.png new file mode 100644 index 000000000..9a5aada8b Binary files /dev/null and b/res/drawable/ic_dialog_keyboard.png differ diff --git a/res/drawable/ic_suggest_scroll_background.xml b/res/drawable/ic_suggest_scroll_background.xml new file mode 100644 index 000000000..9d246e40e --- /dev/null +++ b/res/drawable/ic_suggest_scroll_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/ic_suggest_strip_scroll_left_arrow.png b/res/drawable/ic_suggest_strip_scroll_left_arrow.png new file mode 100644 index 000000000..a9adef2ba Binary files /dev/null and b/res/drawable/ic_suggest_strip_scroll_left_arrow.png differ diff --git a/res/drawable/ic_suggest_strip_scroll_right_arrow.png b/res/drawable/ic_suggest_strip_scroll_right_arrow.png new file mode 100644 index 000000000..639a28711 Binary files /dev/null and b/res/drawable/ic_suggest_strip_scroll_right_arrow.png differ diff --git a/res/drawable/keyboard_suggest_strip.9.png b/res/drawable/keyboard_suggest_strip.9.png new file mode 100644 index 000000000..71bf5e8f4 Binary files /dev/null and b/res/drawable/keyboard_suggest_strip.9.png differ diff --git a/res/drawable/keyboard_suggest_strip_divider.png b/res/drawable/keyboard_suggest_strip_divider.png new file mode 100644 index 000000000..e54c5b099 Binary files /dev/null and b/res/drawable/keyboard_suggest_strip_divider.png differ diff --git a/res/drawable/sym_keyboard_delete.png b/res/drawable/sym_keyboard_delete.png new file mode 100644 index 000000000..f1f7c58cf Binary files /dev/null and b/res/drawable/sym_keyboard_delete.png differ diff --git a/res/drawable/sym_keyboard_done.png b/res/drawable/sym_keyboard_done.png new file mode 100755 index 000000000..c0d6d1394 Binary files /dev/null and b/res/drawable/sym_keyboard_done.png differ diff --git a/res/drawable/sym_keyboard_feedback_delete.png b/res/drawable/sym_keyboard_feedback_delete.png new file mode 100644 index 000000000..3c9083972 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_delete.png differ diff --git a/res/drawable/sym_keyboard_feedback_done.png b/res/drawable/sym_keyboard_feedback_done.png new file mode 100755 index 000000000..0d7ebd4e5 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_done.png differ diff --git a/res/drawable/sym_keyboard_feedback_numalt.png b/res/drawable/sym_keyboard_feedback_numalt.png new file mode 100644 index 000000000..aac737615 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_numalt.png differ diff --git a/res/drawable/sym_keyboard_feedback_numpound.png b/res/drawable/sym_keyboard_feedback_numpound.png new file mode 100644 index 000000000..6b6561e80 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_numpound.png differ diff --git a/res/drawable/sym_keyboard_feedback_numstar.png b/res/drawable/sym_keyboard_feedback_numstar.png new file mode 100644 index 000000000..05f7b4fc9 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_numstar.png differ diff --git a/res/drawable/sym_keyboard_feedback_return.png b/res/drawable/sym_keyboard_feedback_return.png new file mode 100644 index 000000000..03d9c9b2d Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_return.png differ diff --git a/res/drawable/sym_keyboard_feedback_search.png b/res/drawable/sym_keyboard_feedback_search.png new file mode 100755 index 000000000..f4af341eb Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_search.png differ diff --git a/res/drawable/sym_keyboard_feedback_shift.png b/res/drawable/sym_keyboard_feedback_shift.png new file mode 100644 index 000000000..97f4661f8 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_shift.png differ diff --git a/res/drawable/sym_keyboard_feedback_shift_locked.png b/res/drawable/sym_keyboard_feedback_shift_locked.png new file mode 100755 index 000000000..7194b30b0 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_shift_locked.png differ diff --git a/res/drawable/sym_keyboard_feedback_space.png b/res/drawable/sym_keyboard_feedback_space.png new file mode 100644 index 000000000..739db6879 Binary files /dev/null and b/res/drawable/sym_keyboard_feedback_space.png differ diff --git a/res/drawable/sym_keyboard_num0.png b/res/drawable/sym_keyboard_num0.png new file mode 100644 index 000000000..e7007c871 Binary files /dev/null and b/res/drawable/sym_keyboard_num0.png differ diff --git a/res/drawable/sym_keyboard_num1.png b/res/drawable/sym_keyboard_num1.png new file mode 100644 index 000000000..aaac11b0c Binary files /dev/null and b/res/drawable/sym_keyboard_num1.png differ diff --git a/res/drawable/sym_keyboard_num2.png b/res/drawable/sym_keyboard_num2.png new file mode 100644 index 000000000..4372eb8f0 Binary files /dev/null and b/res/drawable/sym_keyboard_num2.png differ diff --git a/res/drawable/sym_keyboard_num3.png b/res/drawable/sym_keyboard_num3.png new file mode 100644 index 000000000..6f54c850f Binary files /dev/null and b/res/drawable/sym_keyboard_num3.png differ diff --git a/res/drawable/sym_keyboard_num4.png b/res/drawable/sym_keyboard_num4.png new file mode 100644 index 000000000..3e50bb957 Binary files /dev/null and b/res/drawable/sym_keyboard_num4.png differ diff --git a/res/drawable/sym_keyboard_num5.png b/res/drawable/sym_keyboard_num5.png new file mode 100644 index 000000000..c39ef4404 Binary files /dev/null and b/res/drawable/sym_keyboard_num5.png differ diff --git a/res/drawable/sym_keyboard_num6.png b/res/drawable/sym_keyboard_num6.png new file mode 100644 index 000000000..ea88ceb94 Binary files /dev/null and b/res/drawable/sym_keyboard_num6.png differ diff --git a/res/drawable/sym_keyboard_num7.png b/res/drawable/sym_keyboard_num7.png new file mode 100644 index 000000000..4d75583af Binary files /dev/null and b/res/drawable/sym_keyboard_num7.png differ diff --git a/res/drawable/sym_keyboard_num8.png b/res/drawable/sym_keyboard_num8.png new file mode 100644 index 000000000..1a8ff94bf Binary files /dev/null and b/res/drawable/sym_keyboard_num8.png differ diff --git a/res/drawable/sym_keyboard_num9.png b/res/drawable/sym_keyboard_num9.png new file mode 100644 index 000000000..8b344c0a6 Binary files /dev/null and b/res/drawable/sym_keyboard_num9.png differ diff --git a/res/drawable/sym_keyboard_numalt.png b/res/drawable/sym_keyboard_numalt.png new file mode 100644 index 000000000..32a2cf3ca Binary files /dev/null and b/res/drawable/sym_keyboard_numalt.png differ diff --git a/res/drawable/sym_keyboard_numpound.png b/res/drawable/sym_keyboard_numpound.png new file mode 100644 index 000000000..b2419d9ab Binary files /dev/null and b/res/drawable/sym_keyboard_numpound.png differ diff --git a/res/drawable/sym_keyboard_numstar.png b/res/drawable/sym_keyboard_numstar.png new file mode 100644 index 000000000..cb66f968f Binary files /dev/null and b/res/drawable/sym_keyboard_numstar.png differ diff --git a/res/drawable/sym_keyboard_return.png b/res/drawable/sym_keyboard_return.png new file mode 100644 index 000000000..17f257439 Binary files /dev/null and b/res/drawable/sym_keyboard_return.png differ diff --git a/res/drawable/sym_keyboard_search.png b/res/drawable/sym_keyboard_search.png new file mode 100755 index 000000000..127755d6b Binary files /dev/null and b/res/drawable/sym_keyboard_search.png differ diff --git a/res/drawable/sym_keyboard_shift.png b/res/drawable/sym_keyboard_shift.png new file mode 100644 index 000000000..0566e5a88 Binary files /dev/null and b/res/drawable/sym_keyboard_shift.png differ diff --git a/res/drawable/sym_keyboard_shift_locked.png b/res/drawable/sym_keyboard_shift_locked.png new file mode 100755 index 000000000..ccaf05d3b Binary files /dev/null and b/res/drawable/sym_keyboard_shift_locked.png differ diff --git a/res/drawable/sym_keyboard_space.png b/res/drawable/sym_keyboard_space.png new file mode 100644 index 000000000..4e6273b89 Binary files /dev/null and b/res/drawable/sym_keyboard_space.png differ diff --git a/res/layout/bubble_text.xml b/res/layout/bubble_text.xml new file mode 100644 index 000000000..af8abce47 --- /dev/null +++ b/res/layout/bubble_text.xml @@ -0,0 +1,27 @@ + + + + diff --git a/res/layout/candidate_preview.xml b/res/layout/candidate_preview.xml new file mode 100755 index 000000000..fe2002d46 --- /dev/null +++ b/res/layout/candidate_preview.xml @@ -0,0 +1,29 @@ + + + + diff --git a/res/layout/candidates.xml b/res/layout/candidates.xml new file mode 100755 index 000000000..edd779a81 --- /dev/null +++ b/res/layout/candidates.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/input.xml b/res/layout/input.xml new file mode 100755 index 000000000..c4bcc91c3 --- /dev/null +++ b/res/layout/input.xml @@ -0,0 +1,27 @@ + + + + diff --git a/res/raw/main.dict b/res/raw/main.dict new file mode 100755 index 000000000..2b473be75 Binary files /dev/null and b/res/raw/main.dict differ diff --git a/res/raw/type3.ogg b/res/raw/type3.ogg new file mode 100755 index 000000000..20e670807 Binary files /dev/null and b/res/raw/type3.ogg differ diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml new file mode 100644 index 000000000..408038506 --- /dev/null +++ b/res/values-cs/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Při stisku klávesy vibrovat" + "Zvuk při stisku klávesy" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Automatická interpunkce" + + + + + + + + + + + + + + + "%s : Uloženo" + "áàâãäåæ" + "éěèêë" + "íìîï" + "óòôõöœø" + "ůúùûü" + "š§ß" + "ňñ" + "čç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml new file mode 100644 index 000000000..4c9920865 --- /dev/null +++ b/res/values-de/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Vibrieren auf Tastendruck" + "Sound bei Tastendruck" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Autom. Zeichensetzung" + + + + + + + + + + + + + + + "%s : Gespeichert" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-en/bools.xml b/res/values-en/bools.xml new file mode 100644 index 000000000..897f4b3db --- /dev/null +++ b/res/values-en/bools.xml @@ -0,0 +1,22 @@ + + + + true + diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml new file mode 100644 index 000000000..08815ee7c --- /dev/null +++ b/res/values-es/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Vibrar al pulsar tecla" + "Sonido al pulsar una tecla" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Puntuación automática" + + + + + + + + + + + + + + + "%s : Guardada" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-fr/donottranslate.xml b/res/values-fr/donottranslate.xml new file mode 100644 index 000000000..527f15e94 --- /dev/null +++ b/res/values-fr/donottranslate.xml @@ -0,0 +1,25 @@ + + + + + .\u0020,;:!?\'\n()[]*&@{}/<>_+=|\u0022 + + ., + diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml new file mode 100644 index 000000000..767004d55 --- /dev/null +++ b/res/values-fr/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Vibrer à chaque touche enfoncée" + "Son à chaque touche enfoncée" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Ponctuation automatique" + + + + + + + + + + + + + + + "%s : enregistré" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-it/donottranslate.xml b/res/values-it/donottranslate.xml new file mode 100644 index 000000000..5cb72adf5 --- /dev/null +++ b/res/values-it/donottranslate.xml @@ -0,0 +1,23 @@ + + + + + .\u0020,;:!?\'\n()[]*&@{}/<>_+=|\u0022 + diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml new file mode 100644 index 000000000..9fd770ac1 --- /dev/null +++ b/res/values-it/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Vibra quando premi un tasto" + "Suona quando premi un tasto" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Punteggiatura automatica" + + + + + + + + + + + + + + + "%s : parola salvata" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml new file mode 100644 index 000000000..8a6c76f96 --- /dev/null +++ b/res/values-ja/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "キーのバイブレーション" + "キーを押したときの音" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "句読点を自動入力" + + + + + + + + + + + + + + + "%s:保存しました" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml new file mode 100644 index 000000000..96d92952a --- /dev/null +++ b/res/values-ko/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "키를 누를 때 진동 발생" + "버튼을 누를 때 소리 발생" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "자동 구두점 입력" + + + + + + + + + + + + + + + "%s : 저장됨" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml new file mode 100644 index 000000000..c5c828ef6 --- /dev/null +++ b/res/values-land/dimens.xml @@ -0,0 +1,23 @@ + + + + + 47dip + \ No newline at end of file diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml new file mode 100644 index 000000000..a01031852 --- /dev/null +++ b/res/values-nb/strings.xml @@ -0,0 +1,92 @@ + + + + + + + + "Vibrer ved tastetrykk" + "Lyd ved tastetrykk" + + + + + + + + + + + "Autokorriger forrige ord ved mellomrom eller linjeskift" + + + + + + + + + + + + + "Stor forbokstav" + "Start automatisk setninger med stor bokstav" + "Automatisk punktum" + + + + + + + + + + + + + + + "%s: Lagret" + "åæáàâãä" + "éèêë" + "íìîï" + "ôóòöõœø" + "üùúû" + "ߧ" + "ñ" + "ç" + "ýÿ" + + + + + + + + + "Trykk lenge på ordet lengst til venstre for å legge det til i ordlisten" + "Gå" + + + + + "?123" + "123" + "ABC" + "ALT" + diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml new file mode 100644 index 000000000..b172defbd --- /dev/null +++ b/res/values-nl/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Trillen bij druk op een toets" + "Geluid bij druk op een toets" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Automatische interpunctie" + + + + + + + + + + + + + + + "%s : Opgeslagen" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml new file mode 100644 index 000000000..eb1a12869 --- /dev/null +++ b/res/values-pl/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Wibruj przy naciśnięciu klawisza" + "Dźwięk przy naciśnięciu klawisza" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Automatyczna interpunkcja" + + + + + + + + + + + + + + + "%s : Zapisano" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml new file mode 100644 index 000000000..7ca302b74 --- /dev/null +++ b/res/values-ru/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "Вибрация при нажатии клавиш" + "Звук при нажатии клавиш" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Автоматическая пунктуация" + + + + + + + + + + + + + + + "%s : сохранено" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..3525a487b --- /dev/null +++ b/res/values-zh-rCN/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "按键时振动" + "按键时发出声音" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "自动加标点" + + + + + + + + + + + + + + + "%s:已保存" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..f880fd7e9 --- /dev/null +++ b/res/values-zh-rTW/strings.xml @@ -0,0 +1,101 @@ + + + + + + + + "按下按鍵時震動" + "按下按鍵時播放音效" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "自動標點" + + + + + + + + + + + + + + + "%s:已儲存" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóôõöœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 000000000..c90d9f6af --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,24 @@ + + + + #FF000000 + #FFE35900 + #ff808080 + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml new file mode 100644 index 000000000..d757f096d --- /dev/null +++ b/res/values/dimens.xml @@ -0,0 +1,24 @@ + + + + + 50dip + 22dip + \ No newline at end of file diff --git a/res/values/donottranslate.xml b/res/values/donottranslate.xml new file mode 100644 index 000000000..8a879bdd5 --- /dev/null +++ b/res/values/donottranslate.xml @@ -0,0 +1,25 @@ + + + + + .\u0020,;:!?\n()[]*&@{}/<>_+=|\u0022 + + .,;:!? + diff --git a/res/values/durations.xml b/res/values/durations.xml new file mode 100644 index 000000000..92af68e39 --- /dev/null +++ b/res/values/durations.xml @@ -0,0 +1,25 @@ + + + + + + 40 + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 000000000..41809c15b --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,142 @@ + + + + + Android keyboard + + Android keyboard settings + + + Vibrate on keypress + + Sound on keypress + + + Correct typing errors + + + Enable input error correction + + + Landscape input errors + + + Enable input error correction + + + Word suggestions + + + Automatically correct the previous word + + + Word suggestions + + Word suggestion settings + + Enable auto completion while typing + + + Auto completion + + + Increase text field size + + Hide word suggestions in landscape view + + + Auto-capitalization + + Capitalize the start of a sentence + + Auto-punctuate + + + + + + None + Basic + Advanced + + + + 0 + + 1 + + 2 + + + @string/prediction_none + @string/prediction_basic + @string/prediction_full + + + + %s : Saved + + àáâãäåæ + + èéêë + + ìíîï + + òóôõöœø + + ùúûü + + §ß + + ñ + + ç + + ýÿ + + + + + Hold a key down to see accents (ø, ö, etc.) + + Press the back key \u21B6 to close the keyboard at any point + + Access numbers and symbols + + Press and hold the left-most word to add it to the dictionary + + + + Go + + Next + + Done + + Send + + \?123 + + 123 + + ABC + + ALT + diff --git a/res/xml-de/kbd_qwerty.xml b/res/xml-de/kbd_qwerty.xml new file mode 100755 index 000000000..763492db5 --- /dev/null +++ b/res/xml-de/kbd_qwerty.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml-fr/kbd_qwerty.xml b/res/xml-fr/kbd_qwerty.xml new file mode 100644 index 000000000..573f08a3b --- /dev/null +++ b/res/xml-fr/kbd_qwerty.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/azerty.xml b/res/xml/azerty.xml new file mode 100644 index 000000000..614aa4936 --- /dev/null +++ b/res/xml/azerty.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/kbd_alpha.xml b/res/xml/kbd_alpha.xml new file mode 100644 index 000000000..4e8af3399 --- /dev/null +++ b/res/xml/kbd_alpha.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/kbd_phone.xml b/res/xml/kbd_phone.xml new file mode 100755 index 000000000..880d9613a --- /dev/null +++ b/res/xml/kbd_phone.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/kbd_phone_symbols.xml b/res/xml/kbd_phone_symbols.xml new file mode 100755 index 000000000..9ce7a1a1f --- /dev/null +++ b/res/xml/kbd_phone_symbols.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/kbd_popup_template.xml b/res/xml/kbd_popup_template.xml new file mode 100644 index 000000000..aca46930f --- /dev/null +++ b/res/xml/kbd_popup_template.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/res/xml/kbd_qwerty.xml b/res/xml/kbd_qwerty.xml new file mode 100755 index 000000000..34e991239 --- /dev/null +++ b/res/xml/kbd_qwerty.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/kbd_symbols.xml b/res/xml/kbd_symbols.xml new file mode 100755 index 000000000..2a150397b --- /dev/null +++ b/res/xml/kbd_symbols.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/kbd_symbols_shift.xml b/res/xml/kbd_symbols_shift.xml new file mode 100755 index 000000000..6a472a412 --- /dev/null +++ b/res/xml/kbd_symbols_shift.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/method.xml b/res/xml/method.xml new file mode 100644 index 000000000..e5654e96d --- /dev/null +++ b/res/xml/method.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/res/xml/popup_domains.xml b/res/xml/popup_domains.xml new file mode 100644 index 000000000..5c86386d5 --- /dev/null +++ b/res/xml/popup_domains.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/res/xml/popup_punctuation.xml b/res/xml/popup_punctuation.xml new file mode 100644 index 000000000..9d4575db6 --- /dev/null +++ b/res/xml/popup_punctuation.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/res/xml/popup_smileys.xml b/res/xml/popup_smileys.xml new file mode 100644 index 000000000..5663fefc8 --- /dev/null +++ b/res/xml/popup_smileys.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml new file mode 100644 index 000000000..1721384f1 --- /dev/null +++ b/res/xml/prefs.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/inputmethod/latin/BinaryDictionary.java b/src/com/android/inputmethod/latin/BinaryDictionary.java new file mode 100644 index 000000000..bb4f1ba46 --- /dev/null +++ b/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.util.Arrays; + +import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; + +/** + * Implements a static, compacted, binary dictionary of standard words. + */ +public class BinaryDictionary extends Dictionary { + + public static final int MAX_WORD_LENGTH = 48; + private static final int MAX_ALTERNATIVES = 16; + private static final int MAX_WORDS = 16; + + private static final int TYPED_LETTER_MULTIPLIER = 2; + + private int mNativeDict; + private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; + private WordCallback mWordCallback; + private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; + private int[] mFrequencies = new int[MAX_WORDS]; + + static { + try { + System.loadLibrary("jni_latinime"); + } catch (UnsatisfiedLinkError ule) { + Log.e("BinaryDictionary", "Could not load native library jni_latinime"); + } + } + + /** + * Create a dictionary from a raw resource file + * @param context application context for reading resources + * @param resId the resource containing the raw binary dictionary + */ + public BinaryDictionary(Context context, int resId) { + if (resId != 0) { + loadDictionary(context, resId); + } + } + + private native int openNative(AssetManager am, String resourcePath, int typedLetterMultiplier, + int fullWordMultiplier); + private native void closeNative(int dict); + private native boolean isValidWordNative(int nativeData, char[] word, int wordLength); + private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize, + char[] outputChars, int[] frequencies, + int maxWordLength, int maxWords, int maxAlternatives); + private native void setParamsNative(int typedLetterMultiplier, + int fullWordMultiplier); + + private final void loadDictionary(Context context, int resId) { + AssetManager am = context.getResources().getAssets(); + String assetName = context.getResources().getString(resId); + mNativeDict = openNative(am, assetName, TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); + } + + @Override + public void getWords(final WordComposer codes, final WordCallback callback) { + mWordCallback = callback; + final int codesSize = codes.size(); + // Wont deal with really long words. + if (codesSize > MAX_WORD_LENGTH - 1) return; + + Arrays.fill(mInputCodes, -1); + for (int i = 0; i < codesSize; i++) { + int[] alternatives = codes.getCodesAt(i); + System.arraycopy(alternatives, 0, mInputCodes, i * MAX_ALTERNATIVES, + Math.min(alternatives.length, MAX_ALTERNATIVES)); + } + Arrays.fill(mOutputChars, (char) 0); + + int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize, mOutputChars, mFrequencies, + MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES); + + for (int j = 0; j < count; j++) { + if (mFrequencies[j] < 1) break; + int start = j * MAX_WORD_LENGTH; + int len = 0; + while (mOutputChars[start + len] != 0) { + len++; + } + if (len > 0) { + callback.addWord(mOutputChars, start, len, mFrequencies[j]); + } + } + } + + @Override + public boolean isValidWord(CharSequence word) { + if (word == null) return false; + char[] chars = word.toString().toLowerCase().toCharArray(); + return isValidWordNative(mNativeDict, chars, chars.length); + } + + public synchronized void close() { + if (mNativeDict != 0) { + closeNative(mNativeDict); + mNativeDict = 0; + } + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } +} diff --git a/src/com/android/inputmethod/latin/CandidateView.java b/src/com/android/inputmethod/latin/CandidateView.java new file mode 100755 index 000000000..08c68dc12 --- /dev/null +++ b/src/com/android/inputmethod/latin/CandidateView.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.PopupWindow; +import android.widget.TextView; + +public class CandidateView extends View { + + private static final int OUT_OF_BOUNDS = -1; + private static final List EMPTY_LIST = new ArrayList(); + + private LatinIME mService; + private List mSuggestions = EMPTY_LIST; + private boolean mShowingCompletions; + private CharSequence mSelectedString; + private int mSelectedIndex; + private int mTouchX = OUT_OF_BOUNDS; + private Drawable mSelectionHighlight; + private boolean mTypedWordValid; + + private boolean mHaveMinimalSuggestion; + + private Rect mBgPadding; + + private TextView mPreviewText; + private PopupWindow mPreviewPopup; + private int mCurrentWordIndex; + private Drawable mDivider; + + private static final int MAX_SUGGESTIONS = 32; + private static final int SCROLL_PIXELS = 20; + + private static final int MSG_REMOVE_PREVIEW = 1; + private static final int MSG_REMOVE_THROUGH_PREVIEW = 2; + + private int[] mWordWidth = new int[MAX_SUGGESTIONS]; + private int[] mWordX = new int[MAX_SUGGESTIONS]; + private int mPopupPreviewX; + private int mPopupPreviewY; + + private static final int X_GAP = 10; + + private int mColorNormal; + private int mColorRecommended; + private int mColorOther; + private Paint mPaint; + private int mDescent; + private boolean mScrolled; + private int mTargetScrollX; + + private int mTotalWidth; + + private GestureDetector mGestureDetector; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REMOVE_PREVIEW: + mPreviewText.setVisibility(GONE); + break; + case MSG_REMOVE_THROUGH_PREVIEW: + mPreviewText.setVisibility(GONE); + if (mTouchX != OUT_OF_BOUNDS) { + removeHighlight(); + } + break; + } + + } + }; + + /** + * Construct a CandidateView for showing suggested words for completion. + * @param context + * @param attrs + */ + public CandidateView(Context context, AttributeSet attrs) { + super(context, attrs); + mSelectionHighlight = context.getResources().getDrawable( + com.android.internal.R.drawable.list_selector_background_pressed); + + LayoutInflater inflate = + (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mPreviewPopup = new PopupWindow(context); + mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null); + mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + mPreviewPopup.setContentView(mPreviewText); + mPreviewPopup.setBackgroundDrawable(null); + mColorNormal = context.getResources().getColor(R.color.candidate_normal); + mColorRecommended = context.getResources().getColor(R.color.candidate_recommended); + mColorOther = context.getResources().getColor(R.color.candidate_other); + mDivider = context.getResources().getDrawable(R.drawable.keyboard_suggest_strip_divider); + + mPaint = new Paint(); + mPaint.setColor(mColorNormal); + mPaint.setAntiAlias(true); + mPaint.setTextSize(mPreviewText.getTextSize()); + mPaint.setStrokeWidth(0); + mDescent = (int) mPaint.descent(); + + mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + @Override + public void onLongPress(MotionEvent me) { + if (mSuggestions.size() > 0) { + if (me.getX() + mScrollX < mWordWidth[0] && mScrollX < 10) { + longPressFirstWord(); + } + } + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + mScrolled = true; + mScrollX += distanceX; + if (mScrollX < 0) { + mScrollX = 0; + } + if (mScrollX + getWidth() > mTotalWidth) { + mScrollX -= distanceX; + } + mTargetScrollX = mScrollX; + showPreview(OUT_OF_BOUNDS, null); + invalidate(); + return true; + } + }); + setHorizontalFadingEdgeEnabled(true); + setWillNotDraw(false); + setHorizontalScrollBarEnabled(false); + setVerticalScrollBarEnabled(false); + mScrollX = 0; + } + + /** + * A connection back to the service to communicate with the text field + * @param listener + */ + public void setService(LatinIME listener) { + mService = listener; + } + + @Override + public int computeHorizontalScrollRange() { + return mTotalWidth; + } + + /** + * If the canvas is null, then only touch calculations are performed to pick the target + * candidate. + */ + @Override + protected void onDraw(Canvas canvas) { + if (canvas != null) { + super.onDraw(canvas); + } + mTotalWidth = 0; + if (mSuggestions == null) return; + + final int height = getHeight(); + if (mBgPadding == null) { + mBgPadding = new Rect(0, 0, 0, 0); + if (getBackground() != null) { + getBackground().getPadding(mBgPadding); + } + mDivider.setBounds(0, mBgPadding.top, mDivider.getIntrinsicWidth(), + mDivider.getIntrinsicHeight()); + } + int x = 0; + final int count = mSuggestions.size(); + final int width = getWidth(); + final Rect bgPadding = mBgPadding; + final Paint paint = mPaint; + final int touchX = mTouchX; + final int scrollX = mScrollX; + final boolean scrolled = mScrolled; + final boolean typedWordValid = mTypedWordValid; + final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2; + + for (int i = 0; i < count; i++) { + CharSequence suggestion = mSuggestions.get(i); + if (suggestion == null) continue; + paint.setColor(mColorNormal); + if (mHaveMinimalSuggestion + && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) { + paint.setTypeface(Typeface.DEFAULT_BOLD); + paint.setColor(mColorRecommended); + } else if (i != 0) { + paint.setColor(mColorOther); + } + final int wordWidth; + if (mWordWidth[i] != 0) { + wordWidth = mWordWidth[i]; + } else { + float textWidth = paint.measureText(suggestion, 0, suggestion.length()); + wordWidth = (int) textWidth + X_GAP * 2; + mWordWidth[i] = wordWidth; + } + + mWordX[i] = x; + + if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled) { + if (canvas != null) { + canvas.translate(x, 0); + mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height); + mSelectionHighlight.draw(canvas); + canvas.translate(-x, 0); + showPreview(i, null); + } + mSelectedString = suggestion; + mSelectedIndex = i; + } + + if (canvas != null) { + canvas.drawText(suggestion, 0, suggestion.length(), x + X_GAP, y, paint); + paint.setColor(mColorOther); + canvas.translate(x + wordWidth, 0); + mDivider.draw(canvas); + canvas.translate(-x - wordWidth, 0); + } + paint.setTypeface(Typeface.DEFAULT); + x += wordWidth; + } + mTotalWidth = x; + if (mTargetScrollX != mScrollX) { + scrollToTarget(); + } + } + + private void scrollToTarget() { + if (mTargetScrollX > mScrollX) { + mScrollX += SCROLL_PIXELS; + if (mScrollX >= mTargetScrollX) { + mScrollX = mTargetScrollX; + requestLayout(); + } + } else { + mScrollX -= SCROLL_PIXELS; + if (mScrollX <= mTargetScrollX) { + mScrollX = mTargetScrollX; + requestLayout(); + } + } + invalidate(); + } + + public void setSuggestions(List suggestions, boolean completions, + boolean typedWordValid, boolean haveMinimalSuggestion) { + clear(); + if (suggestions != null) { + mSuggestions = new ArrayList(suggestions); + } + mShowingCompletions = completions; + mTypedWordValid = typedWordValid; + mScrollX = 0; + mTargetScrollX = 0; + mHaveMinimalSuggestion = haveMinimalSuggestion; + // Compute the total width + onDraw(null); + invalidate(); + requestLayout(); + } + + public void scrollPrev() { + int i = 0; + final int count = mSuggestions.size(); + int firstItem = 0; // Actually just before the first item, if at the boundary + while (i < count) { + if (mWordX[i] < mScrollX + && mWordX[i] + mWordWidth[i] >= mScrollX - 1) { + firstItem = i; + break; + } + i++; + } + int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth(); + if (leftEdge < 0) leftEdge = 0; + updateScrollPosition(leftEdge); + } + + public void scrollNext() { + int i = 0; + int targetX = mScrollX; + final int count = mSuggestions.size(); + int rightEdge = mScrollX + getWidth(); + while (i < count) { + if (mWordX[i] <= rightEdge && + mWordX[i] + mWordWidth[i] >= rightEdge) { + targetX = Math.min(mWordX[i], mTotalWidth - getWidth()); + break; + } + i++; + } + updateScrollPosition(targetX); + } + + private void updateScrollPosition(int targetX) { + if (targetX != mScrollX) { + // TODO: Animate + mTargetScrollX = targetX; + requestLayout(); + invalidate(); + mScrolled = true; + } + } + + public void clear() { + mSuggestions = EMPTY_LIST; + mTouchX = OUT_OF_BOUNDS; + mSelectedString = null; + mSelectedIndex = -1; + invalidate(); + Arrays.fill(mWordWidth, 0); + Arrays.fill(mWordX, 0); + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + + if (mGestureDetector.onTouchEvent(me)) { + return true; + } + + int action = me.getAction(); + int x = (int) me.getX(); + int y = (int) me.getY(); + mTouchX = x; + + switch (action) { + case MotionEvent.ACTION_DOWN: + mScrolled = false; + invalidate(); + break; + case MotionEvent.ACTION_MOVE: + if (y <= 0) { + // Fling up!? + if (mSelectedString != null) { + if (!mShowingCompletions) { + TextEntryState.acceptedSuggestion(mSuggestions.get(0), + mSelectedString); + } + mService.pickSuggestionManually(mSelectedIndex, mSelectedString); + mSelectedString = null; + mSelectedIndex = -1; + } + } + invalidate(); + break; + case MotionEvent.ACTION_UP: + if (!mScrolled) { + if (mSelectedString != null) { + if (!mShowingCompletions) { + TextEntryState.acceptedSuggestion(mSuggestions.get(0), + mSelectedString); + } + mService.pickSuggestionManually(mSelectedIndex, mSelectedString); + } + } + mSelectedString = null; + mSelectedIndex = -1; + removeHighlight(); + showPreview(OUT_OF_BOUNDS, null); + requestLayout(); + break; + } + return true; + } + + /** + * For flick through from keyboard, call this method with the x coordinate of the flick + * gesture. + * @param x + */ + public void takeSuggestionAt(float x) { + mTouchX = (int) x; + // To detect candidate + onDraw(null); + if (mSelectedString != null) { + if (!mShowingCompletions) { + TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString); + } + mService.pickSuggestionManually(mSelectedIndex, mSelectedString); + } + invalidate(); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_REMOVE_THROUGH_PREVIEW), 200); + } + + private void showPreview(int wordIndex, String altText) { + int oldWordIndex = mCurrentWordIndex; + mCurrentWordIndex = wordIndex; + // If index changed or changing text + if (oldWordIndex != mCurrentWordIndex || altText != null) { + if (wordIndex == OUT_OF_BOUNDS) { + if (mPreviewPopup.isShowing()) { + mHandler.sendMessageDelayed(mHandler + .obtainMessage(MSG_REMOVE_PREVIEW), 60); + } + } else { + CharSequence word = altText != null? altText : mSuggestions.get(wordIndex); + mPreviewText.setText(word); + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2); + final int popupWidth = wordWidth + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight(); + final int popupHeight = mPreviewText.getMeasuredHeight(); + //mPreviewText.setVisibility(INVISIBLE); + mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - mScrollX; + mPopupPreviewY = - popupHeight; + mHandler.removeMessages(MSG_REMOVE_PREVIEW); + int [] offsetInWindow = new int[2]; + getLocationInWindow(offsetInWindow); + if (mPreviewPopup.isShowing()) { + mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1], + popupWidth, popupHeight); + } else { + mPreviewPopup.setWidth(popupWidth); + mPreviewPopup.setHeight(popupHeight); + mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX, + mPopupPreviewY + offsetInWindow[1]); + } + mPreviewText.setVisibility(VISIBLE); + } + } + } + + private void removeHighlight() { + mTouchX = OUT_OF_BOUNDS; + invalidate(); + } + + private void longPressFirstWord() { + CharSequence word = mSuggestions.get(0); + if (mService.addWordToDictionary(word.toString())) { + showPreview(0, getContext().getResources().getString(R.string.added_word, word)); + } + } +} diff --git a/src/com/android/inputmethod/latin/CandidateViewContainer.java b/src/com/android/inputmethod/latin/CandidateViewContainer.java new file mode 100644 index 000000000..e13f2738c --- /dev/null +++ b/src/com/android/inputmethod/latin/CandidateViewContainer.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.LinearLayout; + +public class CandidateViewContainer extends LinearLayout implements OnTouchListener { + + private View mButtonLeft; + private View mButtonRight; + private View mButtonLeftLayout; + private View mButtonRightLayout; + private CandidateView mCandidates; + + public CandidateViewContainer(Context screen, AttributeSet attrs) { + super(screen, attrs); + } + + public void initViews() { + if (mCandidates == null) { + mButtonLeftLayout = findViewById(R.id.candidate_left_parent); + mButtonLeft = findViewById(R.id.candidate_left); + if (mButtonLeft != null) { + mButtonLeft.setOnTouchListener(this); + } + mButtonRightLayout = findViewById(R.id.candidate_right_parent); + mButtonRight = findViewById(R.id.candidate_right); + if (mButtonRight != null) { + mButtonRight.setOnTouchListener(this); + } + mCandidates = (CandidateView) findViewById(R.id.candidates); + } + } + + @Override + public void requestLayout() { + if (mCandidates != null) { + int availableWidth = mCandidates.getWidth(); + int neededWidth = mCandidates.computeHorizontalScrollRange(); + int x = mCandidates.getScrollX(); + boolean leftVisible = x > 0; + boolean rightVisible = x + availableWidth < neededWidth; + if (mButtonLeftLayout != null) { + mButtonLeftLayout.setVisibility(leftVisible ? VISIBLE : GONE); + } + if (mButtonRightLayout != null) { + mButtonRightLayout.setVisibility(rightVisible ? VISIBLE : GONE); + } + } + super.requestLayout(); + } + + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (v == mButtonRight) { + mCandidates.scrollNext(); + } else if (v == mButtonLeft) { + mCandidates.scrollPrev(); + } + } + return false; + } + +} diff --git a/src/com/android/inputmethod/latin/Dictionary.java b/src/com/android/inputmethod/latin/Dictionary.java new file mode 100644 index 000000000..fdf34264a --- /dev/null +++ b/src/com/android/inputmethod/latin/Dictionary.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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; + +/** + * Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key + * strokes. + */ +abstract public class Dictionary { + + /** + * Whether or not to replicate the typed word in the suggested list, even if it's valid. + */ + protected static final boolean INCLUDE_TYPED_WORD_IF_VALID = false; + + /** + * The weight to give to a word if it's length is the same as the number of typed characters. + */ + protected static final int FULL_WORD_FREQ_MULTIPLIER = 2; + + /** + * Interface to be implemented by classes requesting words to be fetched from the dictionary. + * @see #getWords(WordComposer, WordCallback) + */ + public interface WordCallback { + /** + * Adds a word to a list of suggestions. The word is expected to be ordered based on + * the provided frequency. + * @param word the character array containing the word + * @param wordOffset starting offset of the word in the character array + * @param wordLength length of valid characters in the character array + * @param frequency the frequency of occurence. This is normalized between 1 and 255, but + * can exceed those limits + * @return true if the word was added, false if no more words are required + */ + boolean addWord(char[] word, int wordOffset, int wordLength, int frequency); + } + + /** + * Searches for words in the dictionary that match the characters in the composer. Matched + * words are added through the callback object. + * @param composer the key sequence to match + * @param callback the callback object to send matched words to as possible candidates + * @see WordCallback#addWord(char[], int, int) + */ + abstract public void getWords(final WordComposer composer, final WordCallback callback); + + /** + * Checks if the given word occurs in the dictionary + * @param word the word to search for. The search should be case-insensitive. + * @return true if the word exists, false otherwise + */ + abstract public boolean isValidWord(CharSequence word); + + /** + * Compares the contents of the character array with the typed word and returns true if they + * are the same. + * @param word the array of characters that make up the word + * @param length the number of valid characters in the character array + * @param typedWord the word to compare with + * @return true if they are the same, false otherwise. + */ + protected boolean same(final char[] word, final int length, final CharSequence typedWord) { + if (typedWord.length() != length) { + return false; + } + for (int i = 0; i < length; i++) { + if (word[i] != typedWord.charAt(i)) { + return false; + } + } + return true; + } + +} diff --git a/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/src/com/android/inputmethod/latin/KeyboardSwitcher.java new file mode 100644 index 000000000..b3fcbda6f --- /dev/null +++ b/src/com/android/inputmethod/latin/KeyboardSwitcher.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * 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.inputmethodservice.Keyboard; + +import java.util.List; + +public class KeyboardSwitcher { + + public static final int MODE_TEXT = 1; + public static final int MODE_SYMBOLS = 2; + public static final int MODE_PHONE = 3; + public static final int MODE_URL = 4; + public static final int MODE_EMAIL = 5; + public static final int MODE_IM = 6; + + public static final int MODE_TEXT_QWERTY = 0; + public static final int MODE_TEXT_ALPHA = 1; + public static final int MODE_TEXT_COUNT = 2; + + public static final int KEYBOARDMODE_NORMAL = R.id.mode_normal; + public static final int KEYBOARDMODE_URL = R.id.mode_url; + public static final int KEYBOARDMODE_EMAIL = R.id.mode_email; + public static final int KEYBOARDMODE_IM = R.id.mode_im; + + + LatinKeyboardView mInputView; + LatinIME mContext; + + private LatinKeyboard mPhoneKeyboard; + private LatinKeyboard mPhoneSymbolsKeyboard; + private LatinKeyboard mSymbolsKeyboard; + private LatinKeyboard mSymbolsShiftedKeyboard; + private LatinKeyboard mQwertyKeyboard; + private LatinKeyboard mAlphaKeyboard; + private LatinKeyboard mUrlKeyboard; + private LatinKeyboard mEmailKeyboard; + private LatinKeyboard mIMKeyboard; + + List mKeyboards; + + private int mMode; + private int mImeOptions; + private int mTextMode = MODE_TEXT_QWERTY; + + private int mLastDisplayWidth; + + KeyboardSwitcher(LatinIME context) { + mContext = context; + } + + void setInputView(LatinKeyboardView inputView) { + mInputView = inputView; + } + + void makeKeyboards() { + // Configuration change is coming after the keyboard gets recreated. So don't rely on that. + // If keyboards have already been made, check if we have a screen width change and + // create the keyboard layouts again at the correct orientation + if (mKeyboards != null) { + int displayWidth = mContext.getMaxWidth(); + if (displayWidth == mLastDisplayWidth) return; + mLastDisplayWidth = displayWidth; + } + // Delayed creation when keyboard mode is set. + mQwertyKeyboard = null; + mAlphaKeyboard = null; + mUrlKeyboard = null; + mEmailKeyboard = null; + mIMKeyboard = null; + mPhoneKeyboard = null; + mPhoneSymbolsKeyboard = null; + mSymbolsKeyboard = null; + mSymbolsShiftedKeyboard = null; + } + + void setKeyboardMode(int mode, int imeOptions) { + mMode = mode; + mImeOptions = imeOptions; + LatinKeyboard keyboard = (LatinKeyboard) mInputView.getKeyboard(); + mInputView.setPreviewEnabled(true); + switch (mode) { + case MODE_TEXT: + if (mTextMode == MODE_TEXT_QWERTY) { + if (mQwertyKeyboard == null) { + mQwertyKeyboard = new LatinKeyboard(mContext, R.xml.kbd_qwerty, + KEYBOARDMODE_NORMAL); + mQwertyKeyboard.enableShiftLock(); + } + keyboard = mQwertyKeyboard; + } else if (mTextMode == MODE_TEXT_ALPHA) { + if (mAlphaKeyboard == null) { + mAlphaKeyboard = new LatinKeyboard(mContext, R.xml.kbd_alpha, + KEYBOARDMODE_NORMAL); + mAlphaKeyboard.enableShiftLock(); + } + keyboard = mAlphaKeyboard; + } + break; + case MODE_SYMBOLS: + if (mSymbolsKeyboard == null) { + mSymbolsKeyboard = new LatinKeyboard(mContext, R.xml.kbd_symbols); + } + if (mSymbolsShiftedKeyboard == null) { + mSymbolsShiftedKeyboard = new LatinKeyboard(mContext, R.xml.kbd_symbols_shift); + } + keyboard = mSymbolsKeyboard; + break; + case MODE_PHONE: + if (mPhoneKeyboard == null) { + mPhoneKeyboard = new LatinKeyboard(mContext, R.xml.kbd_phone); + } + mInputView.setPhoneKeyboard(mPhoneKeyboard); + if (mPhoneSymbolsKeyboard == null) { + mPhoneSymbolsKeyboard = new LatinKeyboard(mContext, R.xml.kbd_phone_symbols); + } + keyboard = mPhoneKeyboard; + mInputView.setPreviewEnabled(false); + break; + case MODE_URL: + if (mUrlKeyboard == null) { + mUrlKeyboard = new LatinKeyboard(mContext, R.xml.kbd_qwerty, KEYBOARDMODE_URL); + mUrlKeyboard.enableShiftLock(); + } + keyboard = mUrlKeyboard; + break; + case MODE_EMAIL: + if (mEmailKeyboard == null) { + mEmailKeyboard = new LatinKeyboard(mContext, R.xml.kbd_qwerty, KEYBOARDMODE_EMAIL); + mEmailKeyboard.enableShiftLock(); + } + keyboard = mEmailKeyboard; + break; + case MODE_IM: + if (mIMKeyboard == null) { + mIMKeyboard = new LatinKeyboard(mContext, R.xml.kbd_qwerty, KEYBOARDMODE_IM); + mIMKeyboard.enableShiftLock(); + } + keyboard = mIMKeyboard; + break; + } + mInputView.setKeyboard(keyboard); + keyboard.setShifted(false); + keyboard.setShiftLocked(keyboard.isShiftLocked()); + keyboard.setImeOptions(mContext.getResources(), mMode, imeOptions); + } + + int getKeyboardMode() { + return mMode; + } + + boolean isTextMode() { + return mMode == MODE_TEXT; + } + + int getTextMode() { + return mTextMode; + } + + void setTextMode(int position) { + if (position < MODE_TEXT_COUNT && position >= 0) { + mTextMode = position; + } + if (isTextMode()) { + setKeyboardMode(MODE_TEXT, mImeOptions); + } + } + + int getTextModeCount() { + return MODE_TEXT_COUNT; + } + + boolean isAlphabetMode() { + Keyboard current = mInputView.getKeyboard(); + if (current == mQwertyKeyboard + || current == mAlphaKeyboard + || current == mUrlKeyboard + || current == mIMKeyboard + || current == mEmailKeyboard) { + return true; + } + return false; + } + + void toggleShift() { + Keyboard currentKeyboard = mInputView.getKeyboard(); + if (currentKeyboard == mSymbolsKeyboard) { + mSymbolsKeyboard.setShifted(true); + mInputView.setKeyboard(mSymbolsShiftedKeyboard); + mSymbolsShiftedKeyboard.setShifted(true); + mSymbolsShiftedKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); + } else if (currentKeyboard == mSymbolsShiftedKeyboard) { + mSymbolsShiftedKeyboard.setShifted(false); + mInputView.setKeyboard(mSymbolsKeyboard); + mSymbolsKeyboard.setShifted(false); + mSymbolsKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); + } + } + + void toggleSymbols() { + Keyboard current = mInputView.getKeyboard(); + if (mSymbolsKeyboard == null) { + mSymbolsKeyboard = new LatinKeyboard(mContext, R.xml.kbd_symbols); + } + if (mSymbolsShiftedKeyboard == null) { + mSymbolsShiftedKeyboard = new LatinKeyboard(mContext, R.xml.kbd_symbols_shift); + } + if (current == mSymbolsKeyboard || current == mSymbolsShiftedKeyboard) { + setKeyboardMode(mMode, mImeOptions); // Could be qwerty, alpha, url, email or im + return; + } else if (current == mPhoneKeyboard) { + current = mPhoneSymbolsKeyboard; + mPhoneSymbolsKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); + } else if (current == mPhoneSymbolsKeyboard) { + current = mPhoneKeyboard; + mPhoneKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); + } else { + current = mSymbolsKeyboard; + mSymbolsKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); + } + mInputView.setKeyboard(current); + if (current == mSymbolsKeyboard) { + current.setShifted(false); + } + } +} diff --git a/src/com/android/inputmethod/latin/LatinIME.java b/src/com/android/inputmethod/latin/LatinIME.java new file mode 100644 index 000000000..8671bf2e5 --- /dev/null +++ b/src/com/android/inputmethod/latin/LatinIME.java @@ -0,0 +1,1091 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.content.res.Configuration; +import android.inputmethodservice.InputMethodService; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.media.AudioManager; +import android.os.Debug; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.os.Vibrator; +import android.preference.PreferenceManager; +import android.text.ClipboardManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.PrintWriterPrinter; +import android.util.Printer; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Input method implementation for Qwerty'ish keyboard. + */ +public class LatinIME extends InputMethodService + implements KeyboardView.OnKeyboardActionListener { + static final boolean DEBUG = false; + static final boolean TRACE = false; + + private static final String PREF_VIBRATE_ON = "vibrate_on"; + private static final String PREF_SOUND_ON = "sound_on"; + private static final String PREF_PROXIMITY_CORRECTION = "hit_correction"; + private static final String PREF_PREDICTION = "prediction_mode"; + private static final String PREF_PREDICTION_LANDSCAPE = "prediction_landscape"; + private static final String PREF_AUTO_CAP = "auto_cap"; + static final String PREF_TUTORIAL_RUN = "tutorial_run"; + + private static final int MSG_UPDATE_SUGGESTIONS = 0; + private static final int MSG_CHECK_TUTORIAL = 1; + + // How many continuous deletes at which to start deleting at a higher speed. + private static final int DELETE_ACCELERATE_AT = 20; + // Key events coming any faster than this are long-presses. + private static final int QUICK_PRESS = 200; + + private static final int KEYCODE_ENTER = 10; + private static final int KEYCODE_SPACE = ' '; + + // Contextual menu positions + private static final int POS_SETTINGS = 0; + private static final int POS_METHOD = 1; + + private LatinKeyboardView mInputView; + private CandidateViewContainer mCandidateViewContainer; + private CandidateView mCandidateView; + private Suggest mSuggest; + private CompletionInfo[] mCompletions; + + private AlertDialog mOptionsDialog; + + private KeyboardSwitcher mKeyboardSwitcher; + + private UserDictionary mUserDictionary; + + private String mLocale; + + private StringBuilder mComposing = new StringBuilder(); + private WordComposer mWord = new WordComposer(); + private int mCommittedLength; + private boolean mPredicting; + private CharSequence mBestWord; + private boolean mPredictionOn; + private boolean mCompletionOn; + private boolean mPasswordMode; + private boolean mAutoSpace; + private boolean mAutoCorrectOn; + private boolean mCapsLock; + private long mLastShiftTime; + private boolean mVibrateOn; + private boolean mSoundOn; + private boolean mProximityCorrection; + private int mCorrectionMode; + private boolean mAutoCap; + private boolean mAutoPunctuate; + private boolean mTutorialShownBefore; + // Indicates whether the suggestion strip is to be on in landscape + private boolean mShowSuggestInLand; + private boolean mJustAccepted; + private CharSequence mJustRevertedSeparator; + private int mDeleteCount; + private long mLastKeyTime; + + private Tutorial mTutorial; + + private Vibrator mVibrator; + private long mVibrateDuration; + + private AudioManager mAudioManager; + private final float FX_VOLUME = 1.0f; + private boolean mSilentMode; + + private String mWordSeparators; + private String mSentenceSeparators; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_SUGGESTIONS: + updateSuggestions(); + break; + case MSG_CHECK_TUTORIAL: + if (!mTutorialShownBefore) { + mTutorial = new Tutorial(mInputView); + mTutorial.start(); + } + break; + } + } + }; + + @Override public void onCreate() { + super.onCreate(); + //setStatusIcon(R.drawable.ime_qwerty); + mKeyboardSwitcher = new KeyboardSwitcher(this); + initSuggest(getResources().getConfiguration().locale.toString()); + + mVibrateDuration = getResources().getInteger(R.integer.vibrate_duration_ms); + + // register to receive ringer mode changes for silent mode + IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION); + registerReceiver(mReceiver, filter); + } + + private void initSuggest(String locale) { + mLocale = locale; + mSuggest = new Suggest(this, R.raw.main); + mSuggest.setCorrectionMode(mCorrectionMode); + mUserDictionary = new UserDictionary(this); + mSuggest.setUserDictionary(mUserDictionary); + mWordSeparators = getResources().getString(R.string.word_separators); + mSentenceSeparators = getResources().getString(R.string.sentence_separators); + } + + @Override public void onDestroy() { + mUserDictionary.close(); + unregisterReceiver(mReceiver); + super.onDestroy(); + } + + @Override + public void onConfigurationChanged(Configuration conf) { + if (!TextUtils.equals(conf.locale.toString(), mLocale)) { + initSuggest(conf.locale.toString()); + } + if (!mTutorialShownBefore && mTutorial != null) { + mTutorial.close(false); + } + super.onConfigurationChanged(conf); + } + + @Override + public View onCreateInputView() { + mInputView = (LatinKeyboardView) getLayoutInflater().inflate( + R.layout.input, null); + mKeyboardSwitcher.setInputView(mInputView); + mKeyboardSwitcher.makeKeyboards(); + mInputView.setOnKeyboardActionListener(this); + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, 0); + return mInputView; + } + + @Override + public View onCreateCandidatesView() { + mKeyboardSwitcher.makeKeyboards(); + mCandidateViewContainer = (CandidateViewContainer) getLayoutInflater().inflate( + R.layout.candidates, null); + mCandidateViewContainer.initViews(); + mCandidateView = (CandidateView) mCandidateViewContainer.findViewById(R.id.candidates); + mCandidateView.setService(this); + setCandidatesViewShown(true); + return mCandidateViewContainer; + } + + @Override + public void onStartInputView(EditorInfo attribute, boolean restarting) { + // In landscape mode, this method gets called without the input view being created. + if (mInputView == null) { + return; + } + + mKeyboardSwitcher.makeKeyboards(); + + TextEntryState.newSession(this); + + mPredictionOn = false; + mCompletionOn = false; + mCompletions = null; + mCapsLock = false; + switch (attribute.inputType&EditorInfo.TYPE_MASK_CLASS) { + case EditorInfo.TYPE_CLASS_NUMBER: + case EditorInfo.TYPE_CLASS_DATETIME: + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, + attribute.imeOptions); + mKeyboardSwitcher.toggleSymbols(); + break; + case EditorInfo.TYPE_CLASS_PHONE: + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_PHONE, + attribute.imeOptions); + break; + case EditorInfo.TYPE_CLASS_TEXT: + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, + attribute.imeOptions); + //startPrediction(); + mPredictionOn = true; + // Make sure that passwords are not displayed in candidate view + int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION; + if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD || + variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) { + mPasswordMode = true; + mPredictionOn = false; + } else { + mPasswordMode = false; + } + if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS + || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) { + mAutoSpace = false; + } else { + mAutoSpace = true; + } + if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { + mPredictionOn = false; + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_EMAIL, + attribute.imeOptions); + } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_URI) { + mPredictionOn = false; + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_URL, + attribute.imeOptions); + } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_IM, + attribute.imeOptions); + } else if (variation == EditorInfo.TYPE_TEXT_VARIATION_FILTER) { + mPredictionOn = false; + } + if ((attribute.inputType&EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { + mPredictionOn = false; + mCompletionOn = true && isFullscreenMode(); + } + updateShiftKeyState(attribute); + break; + default: + mKeyboardSwitcher.setKeyboardMode(KeyboardSwitcher.MODE_TEXT, + attribute.imeOptions); + updateShiftKeyState(attribute); + } + mInputView.closing(); + mComposing.setLength(0); + mPredicting = false; + mDeleteCount = 0; + setCandidatesViewShown(false); + if (mCandidateView != null) mCandidateView.setSuggestions(null, false, false, false); + loadSettings(); + mInputView.setProximityCorrectionEnabled(mProximityCorrection); + if (mSuggest != null) { + mSuggest.setCorrectionMode(mCorrectionMode); + } + if (!mTutorialShownBefore && mTutorial == null) { + mHandler.sendEmptyMessageDelayed(MSG_CHECK_TUTORIAL, 1000); + } + mPredictionOn = mPredictionOn && mCorrectionMode > 0; + if (TRACE) Debug.startMethodTracing("latinime"); + } + + @Override + public void onFinishInput() { + super.onFinishInput(); + + if (mInputView != null) { + mInputView.closing(); + } + if (!mTutorialShownBefore && mTutorial != null) { + mTutorial.close(false); + } + } + + @Override + public void onUpdateSelection(int oldSelStart, int oldSelEnd, + int newSelStart, int newSelEnd, + int candidatesStart, int candidatesEnd) { + super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, + candidatesStart, candidatesEnd); + // If the current selection in the text view changes, we should + // clear whatever candidate text we have. + if (mComposing.length() > 0 && mPredicting && (newSelStart != candidatesEnd + || newSelEnd != candidatesEnd)) { + mComposing.setLength(0); + mPredicting = false; + updateSuggestions(); + TextEntryState.reset(); + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.finishComposingText(); + } + } else if (!mPredicting && !mJustAccepted + && TextEntryState.getState() == TextEntryState.STATE_ACCEPTED_DEFAULT) { + TextEntryState.reset(); + } + mJustAccepted = false; + } + + @Override + public void hideWindow() { + if (TRACE) Debug.stopMethodTracing(); + super.hideWindow(); + TextEntryState.endSession(); + } + + @Override + public void onDisplayCompletions(CompletionInfo[] completions) { + if (false) { + Log.i("foo", "Received completions:"); + for (int i=0; i<(completions != null ? completions.length : 0); i++) { + Log.i("foo", " #" + i + ": " + completions[i]); + } + } + if (mCompletionOn) { + mCompletions = completions; + if (completions == null) { + mCandidateView.setSuggestions(null, false, false, false); + return; + } + + List stringList = new ArrayList(); + for (int i=0; i<(completions != null ? completions.length : 0); i++) { + CompletionInfo ci = completions[i]; + if (ci != null) stringList.add(ci.getText()); + } + //CharSequence typedWord = mWord.getTypedWord(); + mCandidateView.setSuggestions(stringList, true, true, true); + mBestWord = null; + setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); + } + } + + @Override + public void setCandidatesViewShown(boolean shown) { + // TODO: Remove this if we support candidates with hard keyboard + if (onEvaluateInputViewShown()) { + super.setCandidatesViewShown(shown); + } + } + + @Override + public void onComputeInsets(InputMethodService.Insets outInsets) { + super.onComputeInsets(outInsets); + outInsets.contentTopInsets = outInsets.visibleTopInsets; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if (event.getRepeatCount() == 0 && mInputView != null) { + if (mInputView.handleBack()) { + return true; + } else if (!mTutorialShownBefore && mTutorial != null) { + mTutorial.close(true); + } + } + break; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + // Enable shift key and DPAD to do selections + if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) { + event = new KeyEvent(event.getDownTime(), event.getEventTime(), + event.getAction(), event.getKeyCode(), event.getRepeatCount(), + event.getDeviceId(), event.getScanCode(), + KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON); + InputConnection ic = getCurrentInputConnection(); + if (ic != null) ic.sendKeyEvent(event); + return true; + } + break; + } + return super.onKeyUp(keyCode, event); + } + + private void commitTyped(InputConnection inputConnection) { + if (mPredicting) { + mPredicting = false; + if (mComposing.length() > 0) { + if (inputConnection != null) { + inputConnection.commitText(mComposing, 1); + } + mCommittedLength = mComposing.length(); + TextEntryState.acceptedTyped(mComposing); + } + updateSuggestions(); + } + } + + public void updateShiftKeyState(EditorInfo attr) { + InputConnection ic = getCurrentInputConnection(); + if (attr != null && mInputView != null && mKeyboardSwitcher.isAlphabetMode() + && ic != null) { + int caps = 0; + EditorInfo ei = getCurrentInputEditorInfo(); + if (mAutoCap && ei != null && ei.inputType != EditorInfo.TYPE_NULL) { + caps = ic.getCursorCapsMode(attr.inputType); + } + mInputView.setShifted(mCapsLock || caps != 0); + } + } + + private void swapPunctuationAndSpace() { + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + CharSequence lastTwo = ic.getTextBeforeCursor(2, 0); + if (lastTwo != null && lastTwo.length() == 2 + && lastTwo.charAt(0) == KEYCODE_SPACE && isSentenceSeparator(lastTwo.charAt(1))) { + ic.beginBatchEdit(); + ic.deleteSurroundingText(2, 0); + ic.commitText(lastTwo.charAt(1) + " ", 1); + ic.endBatchEdit(); + updateShiftKeyState(getCurrentInputEditorInfo()); + } + } + + private void doubleSpace() { + //if (!mAutoPunctuate) return; + if (mCorrectionMode == Suggest.CORRECTION_NONE) return; + final InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + CharSequence lastThree = ic.getTextBeforeCursor(3, 0); + if (lastThree != null && lastThree.length() == 3 + && Character.isLetterOrDigit(lastThree.charAt(0)) + && lastThree.charAt(1) == KEYCODE_SPACE && lastThree.charAt(2) == KEYCODE_SPACE) { + ic.beginBatchEdit(); + ic.deleteSurroundingText(2, 0); + ic.commitText(". ", 1); + ic.endBatchEdit(); + updateShiftKeyState(getCurrentInputEditorInfo()); + } + } + + public boolean addWordToDictionary(String word) { + mUserDictionary.addWord(word, 128); + return true; + } + + private boolean isAlphabet(int code) { + if (Character.isLetter(code)) { + return true; + } else { + return false; + } + } + + // Implementation of KeyboardViewListener + + public void onKey(int primaryCode, int[] keyCodes) { + long when = SystemClock.uptimeMillis(); + if (primaryCode != Keyboard.KEYCODE_DELETE || + when > mLastKeyTime + QUICK_PRESS) { + mDeleteCount = 0; + } + mLastKeyTime = when; + switch (primaryCode) { + case Keyboard.KEYCODE_DELETE: + handleBackspace(); + mDeleteCount++; + break; + case Keyboard.KEYCODE_SHIFT: + handleShift(); + break; + case Keyboard.KEYCODE_CANCEL: + if (mOptionsDialog == null || !mOptionsDialog.isShowing()) { + handleClose(); + } + break; + case LatinKeyboardView.KEYCODE_OPTIONS: + showOptionsMenu(); + break; + case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS: + if (mCapsLock) { + handleShift(); + } else { + toggleCapsLock(); + } + break; + case Keyboard.KEYCODE_MODE_CHANGE: + changeKeyboardMode(); + break; + default: + if (isWordSeparator(primaryCode)) { + handleSeparator(primaryCode); + } else { + handleCharacter(primaryCode, keyCodes); + } + // Cancel the just reverted state + mJustRevertedSeparator = null; + } + } + + public void onText(CharSequence text) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + ic.beginBatchEdit(); + if (mPredicting) { + commitTyped(ic); + } + ic.commitText(text, 1); + ic.endBatchEdit(); + updateShiftKeyState(getCurrentInputEditorInfo()); + mJustRevertedSeparator = null; + } + + private void handleBackspace() { + boolean deleteChar = false; + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + if (mPredicting) { + final int length = mComposing.length(); + if (length > 0) { + mComposing.delete(length - 1, length); + mWord.deleteLast(); + ic.setComposingText(mComposing, 1); + if (mComposing.length() == 0) { + mPredicting = false; + } + postUpdateSuggestions(); + } else { + ic.deleteSurroundingText(1, 0); + } + } else { + //getCurrentInputConnection().deleteSurroundingText(1, 0); + deleteChar = true; + //sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + TextEntryState.backspace(); + if (TextEntryState.getState() == TextEntryState.STATE_UNDO_COMMIT) { + revertLastWord(deleteChar); + return; + } else if (deleteChar) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + if (mDeleteCount > DELETE_ACCELERATE_AT) { + sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + } + } + mJustRevertedSeparator = null; + } + + private void handleShift() { + Keyboard currentKeyboard = mInputView.getKeyboard(); + if (mKeyboardSwitcher.isAlphabetMode()) { + // Alphabet keyboard + checkToggleCapsLock(); + mInputView.setShifted(mCapsLock || !mInputView.isShifted()); + } else { + mKeyboardSwitcher.toggleShift(); + } + } + + private void handleCharacter(int primaryCode, int[] keyCodes) { + if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) { + if (!mPredicting) { + mPredicting = true; + mComposing.setLength(0); + mWord.reset(); + } + } + if (mInputView.isShifted()) { + primaryCode = Character.toUpperCase(primaryCode); + } + if (mPredicting) { + if (mInputView.isShifted() && mComposing.length() == 0) { + mWord.setCapitalized(true); + } + mComposing.append((char) primaryCode); + mWord.add(primaryCode, keyCodes); + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.setComposingText(mComposing, 1); + } + postUpdateSuggestions(); + } else { + sendKeyChar((char)primaryCode); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + measureCps(); + TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode)); + } + + private void handleSeparator(int primaryCode) { + boolean pickedDefault = false; + // Handle separator + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.beginBatchEdit(); + } + if (mPredicting) { + // In certain languages where single quote is a separator, it's better + // not to auto correct, but accept the typed word. For instance, + // in Italian dov' should not be expanded to dove' because the elision + // requires the last vowel to be removed. + if (mAutoCorrectOn && primaryCode != '\'' && + (mJustRevertedSeparator == null + || mJustRevertedSeparator.length() == 0 + || mJustRevertedSeparator.charAt(0) != primaryCode)) { + pickDefaultSuggestion(); + pickedDefault = true; + } else { + commitTyped(ic); + } + } + sendKeyChar((char)primaryCode); + TextEntryState.typedCharacter((char) primaryCode, true); + if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED + && primaryCode != KEYCODE_ENTER) { + swapPunctuationAndSpace(); + } else if (isPredictionOn() && primaryCode == ' ') { + //else if (TextEntryState.STATE_SPACE_AFTER_ACCEPTED) { + doubleSpace(); + } + if (pickedDefault && mBestWord != null) { + TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + if (ic != null) { + ic.endBatchEdit(); + } + } + + private void handleClose() { + commitTyped(getCurrentInputConnection()); + if (!mTutorialShownBefore && mTutorial != null) { + mTutorial.close(true); + } + requestHideSelf(0); + mInputView.closing(); + TextEntryState.endSession(); + } + + private void checkToggleCapsLock() { + if (mInputView.getKeyboard().isShifted()) { + toggleCapsLock(); + } + } + + private void toggleCapsLock() { + mCapsLock = !mCapsLock; + if (mKeyboardSwitcher.isAlphabetMode()) { + ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + } + } + + private void postUpdateSuggestions() { + mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); + } + + private boolean isPredictionOn() { + boolean predictionOn = mPredictionOn; + //if (isFullscreenMode()) predictionOn &= mPredictionLandscape; + return predictionOn; + } + + private boolean isCandidateStripVisible() { + boolean visible = isPredictionOn() && + (!isFullscreenMode() || + mCorrectionMode == Suggest.CORRECTION_FULL || + mShowSuggestInLand); + return visible; + } + + private void updateSuggestions() { + // Check if we have a suggestion engine attached. + if (mSuggest == null || !isPredictionOn()) { + return; + } + + if (!mPredicting) { + mCandidateView.setSuggestions(null, false, false, false); + return; + } + + List stringList = mSuggest.getSuggestions(mInputView, mWord, false); + boolean correctionAvailable = mSuggest.hasMinimalCorrection(); + //|| mCorrectionMode == mSuggest.CORRECTION_FULL; + CharSequence typedWord = mWord.getTypedWord(); + // If we're in basic correct + boolean typedWordValid = mSuggest.isValidWord(typedWord); + if (mCorrectionMode == Suggest.CORRECTION_FULL) { + correctionAvailable |= typedWordValid; + } + + mCandidateView.setSuggestions(stringList, false, typedWordValid, correctionAvailable); + if (stringList.size() > 0) { + if (correctionAvailable && !typedWordValid && stringList.size() > 1) { + mBestWord = stringList.get(1); + } else { + mBestWord = typedWord; + } + } else { + mBestWord = null; + } + setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); + } + + private void pickDefaultSuggestion() { + // Complete any pending candidate query first + if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) { + mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); + updateSuggestions(); + } + if (mBestWord != null) { + TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); + mJustAccepted = true; + pickSuggestion(mBestWord); + } + } + + public void pickSuggestionManually(int index, CharSequence suggestion) { + if (mCompletionOn && mCompletions != null && index >= 0 + && index < mCompletions.length) { + CompletionInfo ci = mCompletions[index]; + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.commitCompletion(ci); + } + mCommittedLength = suggestion.length(); + if (mCandidateView != null) { + mCandidateView.clear(); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + return; + } + pickSuggestion(suggestion); + TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); + // Follow it with a space + if (mAutoSpace) { + sendSpace(); + } + // Fool the state watcher so that a subsequent backspace will not do a revert + TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); + } + + private void pickSuggestion(CharSequence suggestion) { + if (mCapsLock) { + suggestion = suggestion.toString().toUpperCase(); + } else if (preferCapitalization() + || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted())) { + suggestion = Character.toUpperCase(suggestion.charAt(0)) + + suggestion.subSequence(1, suggestion.length()).toString(); + } + InputConnection ic = getCurrentInputConnection(); + if (ic != null) { + ic.commitText(suggestion, 1); + } + mPredicting = false; + mCommittedLength = suggestion.length(); + if (mCandidateView != null) { + mCandidateView.setSuggestions(null, false, false, false); + } + updateShiftKeyState(getCurrentInputEditorInfo()); + } + + private boolean isCursorTouchingWord() { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return false; + CharSequence toLeft = ic.getTextBeforeCursor(1, 0); + CharSequence toRight = ic.getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(toLeft) + && !isWordSeparator(toLeft.charAt(0))) { + return true; + } + if (!TextUtils.isEmpty(toRight) + && !isWordSeparator(toRight.charAt(0))) { + return true; + } + return false; + } + + public void revertLastWord(boolean deleteChar) { + final int length = mComposing.length(); + if (!mPredicting && length > 0) { + final InputConnection ic = getCurrentInputConnection(); + mPredicting = true; + ic.beginBatchEdit(); + mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0); + if (deleteChar) ic.deleteSurroundingText(1, 0); + int toDelete = mCommittedLength; + CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0); + if (toTheLeft != null && toTheLeft.length() > 0 + && isWordSeparator(toTheLeft.charAt(0))) { + toDelete--; + } + ic.deleteSurroundingText(toDelete, 0); + ic.setComposingText(mComposing, 1); + TextEntryState.backspace(); + ic.endBatchEdit(); + postUpdateSuggestions(); + } else { + sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + mJustRevertedSeparator = null; + } + } + + protected String getWordSeparators() { + return mWordSeparators; + } + + public boolean isWordSeparator(int code) { + String separators = getWordSeparators(); + return separators.contains(String.valueOf((char)code)); + } + + public boolean isSentenceSeparator(int code) { + return mSentenceSeparators.contains(String.valueOf((char)code)); + } + + private void sendSpace() { + sendKeyChar((char)KEYCODE_SPACE); + updateShiftKeyState(getCurrentInputEditorInfo()); + //onKey(KEY_SPACE[0], KEY_SPACE); + } + + public boolean preferCapitalization() { + return mWord.isCapitalized(); + } + + public void swipeRight() { + if (LatinKeyboardView.DEBUG_AUTO_PLAY) { + ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); + CharSequence text = cm.getText(); + if (!TextUtils.isEmpty(text)) { + mInputView.startPlaying(text.toString()); + } + } +// if (mAutoCorrectOn) { +// commitTyped(getCurrentInputConnection()); +// } else if (mPredicting) { +// pickDefaultSuggestion(); +// } +// if (mAutoSpace) { +// sendSpace(); +// } + } + + public void swipeLeft() { + //handleBackspace(); + } + + public void swipeDown() { + //handleClose(); + } + + public void swipeUp() { + //launchSettings(); + } + + public void onPress(int primaryCode) { + vibrate(); + playKeyClick(primaryCode); + } + + public void onRelease(int primaryCode) { + //vibrate(); + } + + // receive ringer mode changes to detect silent mode + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateRingerMode(); + } + }; + + // update flags for silent mode + private void updateRingerMode() { + if (mAudioManager == null) { + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + } + if (mAudioManager != null) { + mSilentMode = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL); + } + } + + private void playKeyClick(int primaryCode) { + // if mAudioManager is null, we don't have the ringer state yet + // mAudioManager will be set by updateRingerMode + if (mAudioManager == null) { + if (mInputView != null) { + updateRingerMode(); + } + } + if (mSoundOn && !mSilentMode) { + // FIXME: Volume and enable should come from UI settings + // FIXME: These should be triggered after auto-repeat logic + int sound = AudioManager.FX_KEYPRESS_STANDARD; + switch (primaryCode) { + case Keyboard.KEYCODE_DELETE: + sound = AudioManager.FX_KEYPRESS_DELETE; + break; + case KEYCODE_ENTER: + sound = AudioManager.FX_KEYPRESS_RETURN; + break; + case KEYCODE_SPACE: + sound = AudioManager.FX_KEYPRESS_SPACEBAR; + break; + } + mAudioManager.playSoundEffect(sound, FX_VOLUME); + } + } + + private void vibrate() { + if (!mVibrateOn) { + return; + } + if (mVibrator == null) { + mVibrator = new Vibrator(); + } + mVibrator.vibrate(mVibrateDuration); + } + + private void launchSettings() { + handleClose(); + Intent intent = new Intent(); + intent.setClass(LatinIME.this, LatinIMESettings.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + private void loadSettings() { + // Get the settings preferences + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + mProximityCorrection = sp.getBoolean(PREF_PROXIMITY_CORRECTION, true); + mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, true); + mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); + String predictionBasic = getString(R.string.prediction_basic); + String mode = sp.getString(PREF_PREDICTION, predictionBasic); + if (mode.equals(getString(R.string.prediction_full))) { + mCorrectionMode = 2; + } else if (mode.equals(predictionBasic)) { + mCorrectionMode = 1; + } else { + mCorrectionMode = 0; + } + mAutoCorrectOn = mSuggest != null && mCorrectionMode > 0; + + mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); + //mAutoPunctuate = sp.getBoolean(PREF_AUTO_PUNCTUATE, mCorrectionMode > 0); + mShowSuggestInLand = !sp.getBoolean(PREF_PREDICTION_LANDSCAPE, false); + mTutorialShownBefore = sp.getBoolean(PREF_TUTORIAL_RUN, false); + } + + private void showOptionsMenu() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setCancelable(true); + builder.setIcon(R.drawable.ic_dialog_keyboard); + builder.setNegativeButton(android.R.string.cancel, null); + CharSequence itemSettings = getString(R.string.english_ime_settings); + CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod); + builder.setItems(new CharSequence[] { + itemSettings, itemInputMethod}, + new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface di, int position) { + di.dismiss(); + switch (position) { + case POS_SETTINGS: + launchSettings(); + break; + case POS_METHOD: + InputMethodManager.getInstance(LatinIME.this).showInputMethodPicker(); + break; + } + } + }); + builder.setTitle(getResources().getString(R.string.english_ime_name)); + mOptionsDialog = builder.create(); + Window window = mOptionsDialog.getWindow(); + WindowManager.LayoutParams lp = window.getAttributes(); + lp.token = mInputView.getWindowToken(); + lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; + window.setAttributes(lp); + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + mOptionsDialog.show(); + } + + private void changeKeyboardMode() { + mKeyboardSwitcher.toggleSymbols(); + if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { + ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + } + + updateShiftKeyState(getCurrentInputEditorInfo()); + } + + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + super.dump(fd, fout, args); + + final Printer p = new PrintWriterPrinter(fout); + p.println("LatinIME state :"); + p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode()); + p.println(" mCapsLock=" + mCapsLock); + p.println(" mComposing=" + mComposing.toString()); + p.println(" mPredictionOn=" + mPredictionOn); + p.println(" mCorrectionMode=" + mCorrectionMode); + p.println(" mPredicting=" + mPredicting); + p.println(" mAutoCorrectOn=" + mAutoCorrectOn); + p.println(" mAutoSpace=" + mAutoSpace); + p.println(" mCompletionOn=" + mCompletionOn); + p.println(" TextEntryState.state=" + TextEntryState.getState()); + p.println(" mSoundOn=" + mSoundOn); + p.println(" mVibrateOn=" + mVibrateOn); + } + + + private static final int[] KEY_SPACE = { KEYCODE_SPACE }; + + + // Characters per second measurement + + private static final boolean PERF_DEBUG = false; + private long mLastCpsTime; + private static final int CPS_BUFFER_SIZE = 16; + private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; + private int mCpsIndex; + + private void measureCps() { + if (!LatinIME.PERF_DEBUG) return; + long now = System.currentTimeMillis(); + if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial + mCpsIntervals[mCpsIndex] = now - mLastCpsTime; + mLastCpsTime = now; + mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE; + long total = 0; + for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i]; + System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total)); + } + +} + + + diff --git a/src/com/android/inputmethod/latin/LatinIMESettings.java b/src/com/android/inputmethod/latin/LatinIMESettings.java new file mode 100644 index 000000000..2c23263ea --- /dev/null +++ b/src/com/android/inputmethod/latin/LatinIMESettings.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; + +public class LatinIMESettings extends PreferenceActivity + implements OnSharedPreferenceChangeListener{ + + private static final String CORRECTION_MODE_KEY = "prediction_mode"; + private static final String PREDICTION_SETTINGS_KEY = "prediction_settings"; + private static final String PREDICTION_LANDSCAPE_KEY = "prediction_landscape"; + + private ListPreference mCorrectionMode; + private PreferenceGroup mPredictionSettings; + private Preference mPredictionLandscape; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.prefs); + mCorrectionMode = (ListPreference) findPreference(CORRECTION_MODE_KEY); + mPredictionSettings = (PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY); + mPredictionLandscape = findPreference(PREDICTION_LANDSCAPE_KEY); + updatePredictionSettings(); + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + protected void onDestroy() { + getPreferenceScreen().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(this); + super.onDestroy(); + } + + private void updatePredictionSettings() { + if (mCorrectionMode != null && mPredictionSettings != null) { + String correctionMode = mCorrectionMode.getValue(); + if (correctionMode.equals(getResources().getString(R.string.prediction_none))) { + mPredictionSettings.setEnabled(false); + } else { + mPredictionSettings.setEnabled(true); + boolean suggestionsInLandscape = + !correctionMode.equals(getResources().getString(R.string.prediction_full)); + mPredictionLandscape.setEnabled(suggestionsInLandscape); + } + } + } + + public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { + if (key.equals(CORRECTION_MODE_KEY)) { + updatePredictionSettings(); + } + } +} diff --git a/src/com/android/inputmethod/latin/LatinKeyboard.java b/src/com/android/inputmethod/latin/LatinKeyboard.java new file mode 100644 index 000000000..94b72b885 --- /dev/null +++ b/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard; +import android.view.inputmethod.EditorInfo; + +public class LatinKeyboard extends Keyboard { + + private Drawable mShiftLockIcon; + private Drawable mShiftLockPreviewIcon; + private Drawable mOldShiftIcon; + private Drawable mOldShiftPreviewIcon; + private Key mShiftKey; + private Key mEnterKey; + + private static final int SHIFT_OFF = 0; + private static final int SHIFT_ON = 1; + private static final int SHIFT_LOCKED = 2; + + private int mShiftState = SHIFT_OFF; + + public LatinKeyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, 0); + } + + public LatinKeyboard(Context context, int xmlLayoutResId, int mode) { + super(context, xmlLayoutResId, mode); + mShiftLockIcon = context.getResources() + .getDrawable(R.drawable.sym_keyboard_shift_locked); + mShiftLockPreviewIcon = context.getResources() + .getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); + mShiftLockPreviewIcon.setBounds(0, 0, + mShiftLockPreviewIcon.getIntrinsicWidth(), + mShiftLockPreviewIcon.getIntrinsicHeight()); + } + + public LatinKeyboard(Context context, int layoutTemplateResId, + CharSequence characters, int columns, int horizontalPadding) { + super(context, layoutTemplateResId, characters, columns, horizontalPadding); + } + + @Override + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + Key key = new LatinKey(res, parent, x, y, parser); + if (key.codes[0] == 10) { + mEnterKey = key; + } + return key; + } + + void setImeOptions(Resources res, int mode, int options) { + if (mEnterKey != null) { + // Reset some of the rarely used attributes. + mEnterKey.popupCharacters = null; + mEnterKey.popupResId = 0; + mEnterKey.text = null; + switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + case EditorInfo.IME_ACTION_GO: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_go_key); + break; + case EditorInfo.IME_ACTION_NEXT: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_next_key); + break; + case EditorInfo.IME_ACTION_DONE: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_done_key); + break; + case EditorInfo.IME_ACTION_SEARCH: + mEnterKey.iconPreview = res.getDrawable( + R.drawable.sym_keyboard_feedback_search); + mEnterKey.icon = res.getDrawable( + R.drawable.sym_keyboard_search); + mEnterKey.label = null; + break; + case EditorInfo.IME_ACTION_SEND: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_send_key); + break; + default: + if (mode == KeyboardSwitcher.MODE_IM) { + mEnterKey.icon = null; + mEnterKey.iconPreview = null; + mEnterKey.label = ":-)"; + mEnterKey.text = ":-) "; + mEnterKey.popupResId = R.xml.popup_smileys; + } else { + mEnterKey.iconPreview = res.getDrawable( + R.drawable.sym_keyboard_feedback_return); + mEnterKey.icon = res.getDrawable( + R.drawable.sym_keyboard_return); + mEnterKey.label = null; + } + break; + } + // Set the initial size of the preview icon + if (mEnterKey.iconPreview != null) { + mEnterKey.iconPreview.setBounds(0, 0, + mEnterKey.iconPreview.getIntrinsicWidth(), + mEnterKey.iconPreview.getIntrinsicHeight()); + } + } + } + + void enableShiftLock() { + int index = getShiftKeyIndex(); + if (index >= 0) { + mShiftKey = getKeys().get(index); + if (mShiftKey instanceof LatinKey) { + ((LatinKey)mShiftKey).enableShiftLock(); + } + mOldShiftIcon = mShiftKey.icon; + mOldShiftPreviewIcon = mShiftKey.iconPreview; + } + } + + void setShiftLocked(boolean shiftLocked) { + if (mShiftKey != null) { + if (shiftLocked) { + mShiftKey.on = true; + mShiftKey.icon = mShiftLockIcon; + mShiftState = SHIFT_LOCKED; + } else { + mShiftKey.on = false; + mShiftKey.icon = mShiftLockIcon; + mShiftState = SHIFT_ON; + } + } + } + + boolean isShiftLocked() { + return mShiftState == SHIFT_LOCKED; + } + + @Override + public boolean setShifted(boolean shiftState) { + boolean shiftChanged = false; + if (mShiftKey != null) { + if (shiftState == false) { + shiftChanged = mShiftState != SHIFT_OFF; + mShiftState = SHIFT_OFF; + mShiftKey.on = false; + mShiftKey.icon = mOldShiftIcon; + } else { + if (mShiftState == SHIFT_OFF) { + shiftChanged = mShiftState == SHIFT_OFF; + mShiftState = SHIFT_ON; + mShiftKey.icon = mShiftLockIcon; + } + } + } else { + return super.setShifted(shiftState); + } + return shiftChanged; + } + + @Override + public boolean isShifted() { + if (mShiftKey != null) { + return mShiftState != SHIFT_OFF; + } else { + return super.isShifted(); + } + } + + static class LatinKey extends Keyboard.Key { + + private boolean mShiftLockEnabled; + + public LatinKey(Resources res, Keyboard.Row parent, int x, int y, + XmlResourceParser parser) { + super(res, parent, x, y, parser); + } + + void enableShiftLock() { + mShiftLockEnabled = true; + } + + @Override + public void onReleased(boolean inside) { + if (!mShiftLockEnabled) { + super.onReleased(inside); + } else { + pressed = !pressed; + } + } + + /** + * Overriding this method so that we can reduce the target area for certain keys. + */ + @Override + public boolean isInside(int x, int y) { + if ((edgeFlags & Keyboard.EDGE_BOTTOM) != 0 || + codes[0] == KEYCODE_SHIFT || + codes[0] == KEYCODE_DELETE) { + y -= height / 10; + } + if (codes[0] == KEYCODE_SHIFT) x += width / 6; + if (codes[0] == KEYCODE_DELETE) x -= width / 6; + return super.isInside(x, y); + } + } +} diff --git a/src/com/android/inputmethod/latin/LatinKeyboardView.java b/src/com/android/inputmethod/latin/LatinKeyboardView.java new file mode 100644 index 000000000..363dcd0b0 --- /dev/null +++ b/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.graphics.Canvas; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.inputmethodservice.Keyboard.Key; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import java.util.List; + +public class LatinKeyboardView extends KeyboardView { + + static final int KEYCODE_OPTIONS = -100; + static final int KEYCODE_SHIFT_LONGPRESS = -101; + + private Keyboard mPhoneKeyboard; + + public LatinKeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setPhoneKeyboard(Keyboard phoneKeyboard) { + mPhoneKeyboard = phoneKeyboard; + } + + @Override + protected boolean onLongPress(Key key) { + if (key.codes[0] == Keyboard.KEYCODE_MODE_CHANGE) { + getOnKeyboardActionListener().onKey(KEYCODE_OPTIONS, null); + return true; + } else if (key.codes[0] == Keyboard.KEYCODE_SHIFT) { + getOnKeyboardActionListener().onKey(KEYCODE_SHIFT_LONGPRESS, null); + invalidate(); + return true; + } else if (key.codes[0] == '0' && getKeyboard() == mPhoneKeyboard) { + // Long pressing on 0 in phone number keypad gives you a '+'. + getOnKeyboardActionListener().onKey('+', null); + return true; + } else { + return super.onLongPress(key); + } + } + + + /**************************** INSTRUMENTATION *******************************/ + + static final boolean DEBUG_AUTO_PLAY = false; + private static final int MSG_TOUCH_DOWN = 1; + private static final int MSG_TOUCH_UP = 2; + + Handler mHandler2; + + private String mStringToPlay; + private int mStringIndex; + private boolean mDownDelivered; + private Key[] mAsciiKeys = new Key[256]; + private boolean mPlaying; + + @Override + public void setKeyboard(Keyboard k) { + super.setKeyboard(k); + if (DEBUG_AUTO_PLAY) { + findKeys(); + if (mHandler2 == null) { + mHandler2 = new Handler() { + @Override + public void handleMessage(Message msg) { + removeMessages(MSG_TOUCH_DOWN); + removeMessages(MSG_TOUCH_UP); + if (mPlaying == false) return; + + switch (msg.what) { + case MSG_TOUCH_DOWN: + if (mStringIndex >= mStringToPlay.length()) { + mPlaying = false; + return; + } + char c = mStringToPlay.charAt(mStringIndex); + while (c > 255 || mAsciiKeys[(int) c] == null) { + mStringIndex++; + if (mStringIndex >= mStringToPlay.length()) { + mPlaying = false; + return; + } + c = mStringToPlay.charAt(mStringIndex); + } + int x = mAsciiKeys[c].x + 10; + int y = mAsciiKeys[c].y + 26; + MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, x, y, 0); + LatinKeyboardView.this.dispatchTouchEvent(me); + me.recycle(); + sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else + // happens + mDownDelivered = true; + break; + case MSG_TOUCH_UP: + char cUp = mStringToPlay.charAt(mStringIndex); + int x2 = mAsciiKeys[cUp].x + 10; + int y2 = mAsciiKeys[cUp].y + 26; + mStringIndex++; + + MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, x2, y2, 0); + LatinKeyboardView.this.dispatchTouchEvent(me2); + me2.recycle(); + sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else + // happens + mDownDelivered = false; + break; + } + } + }; + + } + } + } + + private void findKeys() { + List keys = getKeyboard().getKeys(); + // Get the keys on this keyboard + for (int i = 0; i < keys.size(); i++) { + int code = keys.get(i).codes[0]; + if (code >= 0 && code <= 255) { + mAsciiKeys[code] = keys.get(i); + } + } + } + + void startPlaying(String s) { + if (!DEBUG_AUTO_PLAY) return; + if (s == null) return; + mStringToPlay = s.toLowerCase(); + mPlaying = true; + mDownDelivered = false; + mStringIndex = 0; + mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); + } + + @Override + public void draw(Canvas c) { + super.draw(c); + if (DEBUG_AUTO_PLAY && mPlaying) { + mHandler2.removeMessages(MSG_TOUCH_DOWN); + mHandler2.removeMessages(MSG_TOUCH_UP); + if (mDownDelivered) { + mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); + } else { + mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); + } + } + } +} diff --git a/src/com/android/inputmethod/latin/Suggest.java b/src/com/android/inputmethod/latin/Suggest.java new file mode 100755 index 000000000..91decd66a --- /dev/null +++ b/src/com/android/inputmethod/latin/Suggest.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.text.AutoText; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This class loads a dictionary and provides a list of suggestions for a given sequence of + * characters. This includes corrections and completions. + * @hide pending API Council Approval + */ +public class Suggest implements Dictionary.WordCallback { + + public static final int CORRECTION_NONE = 0; + public static final int CORRECTION_BASIC = 1; + public static final int CORRECTION_FULL = 2; + + private Dictionary mMainDict; + + private Dictionary mUserDictionary; + + private int mPrefMaxSuggestions = 12; + + private int[] mPriorities = new int[mPrefMaxSuggestions]; + private List mSuggestions = new ArrayList(); + private boolean mIncludeTypedWordIfValid; + private List mStringPool = new ArrayList(); + private Context mContext; + private boolean mHaveCorrection; + private CharSequence mOriginalWord; + private String mLowerOriginalWord; + + private int mCorrectionMode = CORRECTION_BASIC; + + + public Suggest(Context context, int dictionaryResId) { + mContext = context; + mMainDict = new BinaryDictionary(context, dictionaryResId); + for (int i = 0; i < mPrefMaxSuggestions; i++) { + StringBuilder sb = new StringBuilder(32); + mStringPool.add(sb); + } + } + + public int getCorrectionMode() { + return mCorrectionMode; + } + + public void setCorrectionMode(int mode) { + mCorrectionMode = mode; + } + + /** + * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted + * before the main dictionary, if set. + */ + public void setUserDictionary(Dictionary userDictionary) { + mUserDictionary = userDictionary; + } + + /** + * Number of suggestions to generate from the input key sequence. This has + * to be a number between 1 and 100 (inclusive). + * @param maxSuggestions + * @throws IllegalArgumentException if the number is out of range + */ + public void setMaxSuggestions(int maxSuggestions) { + if (maxSuggestions < 1 || maxSuggestions > 100) { + throw new IllegalArgumentException("maxSuggestions must be between 1 and 100"); + } + mPrefMaxSuggestions = maxSuggestions; + mPriorities = new int[mPrefMaxSuggestions]; + collectGarbage(); + while (mStringPool.size() < mPrefMaxSuggestions) { + StringBuilder sb = new StringBuilder(32); + mStringPool.add(sb); + } + } + + private boolean haveSufficientCommonality(String original, CharSequence suggestion) { + final int len = Math.min(original.length(), suggestion.length()); + if (len <= 2) return true; + int matching = 0; + for (int i = 0; i < len; i++) { + if (UserDictionary.toLowerCase(original.charAt(i)) + == UserDictionary.toLowerCase(suggestion.charAt(i))) { + matching++; + } + } + if (len <= 4) { + return matching >= 2; + } else { + return matching > len / 2; + } + } + + /** + * Returns a list of words that match the list of character codes passed in. + * This list will be overwritten the next time this function is called. + * @param a view for retrieving the context for AutoText + * @param codes the list of codes. Each list item contains an array of character codes + * in order of probability where the character at index 0 in the array has the highest + * probability. + * @return list of suggestions. + */ + public List getSuggestions(View view, WordComposer wordComposer, + boolean includeTypedWordIfValid) { + mHaveCorrection = false; + collectGarbage(); + Arrays.fill(mPriorities, 0); + mIncludeTypedWordIfValid = includeTypedWordIfValid; + + // Save a lowercase version of the original word + mOriginalWord = wordComposer.getTypedWord(); + if (mOriginalWord != null) { + mOriginalWord = mOriginalWord.toString(); + mLowerOriginalWord = mOriginalWord.toString().toLowerCase(); + } else { + mLowerOriginalWord = ""; + } + // Search the dictionary only if there are at least 2 characters + if (wordComposer.size() > 1) { + if (mUserDictionary != null) { + mUserDictionary.getWords(wordComposer, this); + if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)) { + mHaveCorrection = true; + } + } + mMainDict.getWords(wordComposer, this); + if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) { + mHaveCorrection = true; + } + } + if (mOriginalWord != null) { + mSuggestions.add(0, mOriginalWord.toString()); + } + + // Check if the first suggestion has a minimum number of characters in common + if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 1) { + if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) { + mHaveCorrection = false; + } + } + + int i = 0; + int max = 6; + // Don't autotext the suggestions from the dictionaries + if (mCorrectionMode == CORRECTION_BASIC) max = 1; + while (i < mSuggestions.size() && i < max) { + String suggestedWord = mSuggestions.get(i).toString().toLowerCase(); + CharSequence autoText = + AutoText.get(suggestedWord, 0, suggestedWord.length(), view); + // Is there an AutoText correction? + boolean canAdd = autoText != null; + // Is that correction already the current prediction (or original word)? + canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i)); + // Is that correction already the next predicted word? + if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) { + canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1)); + } + if (canAdd) { + mHaveCorrection = true; + mSuggestions.add(i + 1, autoText); + i++; + } + i++; + } + + return mSuggestions; + } + + public boolean hasMinimalCorrection() { + return mHaveCorrection; + } + + private boolean compareCaseInsensitive(final String mLowerOriginalWord, + final char[] word, final int offset, final int length) { + final int originalLength = mLowerOriginalWord.length(); + if (originalLength == length && Character.isUpperCase(word[offset])) { + for (int i = 0; i < originalLength; i++) { + if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) { + return false; + } + } + return true; + } + return false; + } + + public boolean addWord(final char[] word, final int offset, final int length, final int freq) { + int pos = 0; + final int[] priorities = mPriorities; + final int prefMaxSuggestions = mPrefMaxSuggestions; + // Check if it's the same word, only caps are different + if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) { + pos = 0; + } else { + // Check the last one's priority and bail + if (priorities[prefMaxSuggestions - 1] >= freq) return true; + while (pos < prefMaxSuggestions) { + if (priorities[pos] < freq + || (priorities[pos] == freq && length < mSuggestions + .get(pos).length())) { + break; + } + pos++; + } + } + + if (pos >= prefMaxSuggestions) { + return true; + } + System.arraycopy(priorities, pos, priorities, pos + 1, + prefMaxSuggestions - pos - 1); + priorities[pos] = freq; + int poolSize = mStringPool.size(); + StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) + : new StringBuilder(32); + sb.setLength(0); + sb.append(word, offset, length); + mSuggestions.add(pos, sb); + if (mSuggestions.size() > prefMaxSuggestions) { + CharSequence garbage = mSuggestions.remove(prefMaxSuggestions); + if (garbage instanceof StringBuilder) { + mStringPool.add(garbage); + } + } + return true; + } + + public boolean isValidWord(final CharSequence word) { + if (word == null || word.length() == 0) { + return false; + } + return (mCorrectionMode == CORRECTION_FULL && mMainDict.isValidWord(word)) + || (mCorrectionMode > CORRECTION_NONE && + (mUserDictionary != null && mUserDictionary.isValidWord(word))); + } + + private void collectGarbage() { + int poolSize = mStringPool.size(); + int garbageSize = mSuggestions.size(); + while (poolSize < mPrefMaxSuggestions && garbageSize > 0) { + CharSequence garbage = mSuggestions.get(garbageSize - 1); + if (garbage != null && garbage instanceof StringBuilder) { + mStringPool.add(garbage); + poolSize++; + } + garbageSize--; + } + if (poolSize == mPrefMaxSuggestions + 1) { + Log.w("Suggest", "String pool got too big: " + poolSize); + } + mSuggestions.clear(); + } +} diff --git a/src/com/android/inputmethod/latin/TextEntryState.java b/src/com/android/inputmethod/latin/TextEntryState.java new file mode 100644 index 000000000..90c364a1c --- /dev/null +++ b/src/com/android/inputmethod/latin/TextEntryState.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.text.format.DateFormat; +import android.util.Log; + +import android.inputmethodservice.Keyboard.Key; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Calendar; + +public class TextEntryState { + + private static boolean LOGGING = false; + + private static int sBackspaceCount = 0; + + private static int sAutoSuggestCount = 0; + + private static int sAutoSuggestUndoneCount = 0; + + private static int sManualSuggestCount = 0; + + private static int sWordNotInDictionaryCount = 0; + + private static int sSessionCount = 0; + + private static int sTypedChars; + + private static int sActualChars; + + private static final String[] STATES = { + "Unknown", + "Start", + "In word", + "Accepted default", + "Picked suggestion", + "Punc. after word", + "Punc. after accepted", + "Space after accepted", + "Space after picked", + "Undo commit" + }; + + public static final int STATE_UNKNOWN = 0; + public static final int STATE_START = 1; + public static final int STATE_IN_WORD = 2; + public static final int STATE_ACCEPTED_DEFAULT = 3; + public static final int STATE_PICKED_SUGGESTION = 4; + public static final int STATE_PUNCTUATION_AFTER_WORD = 5; + public static final int STATE_PUNCTUATION_AFTER_ACCEPTED = 6; + public static final int STATE_SPACE_AFTER_ACCEPTED = 7; + public static final int STATE_SPACE_AFTER_PICKED = 8; + public static final int STATE_UNDO_COMMIT = 9; + + private static int sState = STATE_UNKNOWN; + + private static FileOutputStream sKeyLocationFile; + private static FileOutputStream sUserActionFile; + + public static void newSession(Context context) { + sSessionCount++; + sAutoSuggestCount = 0; + sBackspaceCount = 0; + sAutoSuggestUndoneCount = 0; + sManualSuggestCount = 0; + sWordNotInDictionaryCount = 0; + sTypedChars = 0; + sActualChars = 0; + sState = STATE_START; + + if (LOGGING) { + try { + sKeyLocationFile = context.openFileOutput("key.txt", Context.MODE_APPEND); + sUserActionFile = context.openFileOutput("action.txt", Context.MODE_APPEND); + } catch (IOException ioe) { + Log.e("TextEntryState", "Couldn't open file for output: " + ioe); + } + } + } + + public static void endSession() { + if (sKeyLocationFile == null) { + return; + } + try { + sKeyLocationFile.close(); + // Write to log file + // Write timestamp, settings, + String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime()) + .toString() + + " BS: " + sBackspaceCount + + " auto: " + sAutoSuggestCount + + " manual: " + sManualSuggestCount + + " typed: " + sWordNotInDictionaryCount + + " undone: " + sAutoSuggestUndoneCount + + " saved: " + ((float) (sActualChars - sTypedChars) / sActualChars) + + "\n"; + sUserActionFile.write(out.getBytes()); + sUserActionFile.close(); + sKeyLocationFile = null; + sUserActionFile = null; + } catch (IOException ioe) { + + } + } + + public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) { + if (!typedWord.equals(actualWord)) { + sAutoSuggestCount++; + } + sTypedChars += typedWord.length(); + sActualChars += actualWord.length(); + sState = STATE_ACCEPTED_DEFAULT; + } + + public static void acceptedTyped(CharSequence typedWord) { + sWordNotInDictionaryCount++; + sState = STATE_PICKED_SUGGESTION; + } + + public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) { + sManualSuggestCount++; + if (typedWord.equals(actualWord)) { + acceptedTyped(typedWord); + } + sState = STATE_PICKED_SUGGESTION; + } + + public static void typedCharacter(char c, boolean isSeparator) { + boolean isSpace = c == ' '; + switch (sState) { + case STATE_IN_WORD: + if (isSpace || isSeparator) { + sState = STATE_START; + } else { + // State hasn't changed. + } + break; + case STATE_ACCEPTED_DEFAULT: + case STATE_SPACE_AFTER_PICKED: + if (isSpace) { + sState = STATE_SPACE_AFTER_ACCEPTED; + } else if (isSeparator) { + sState = STATE_PUNCTUATION_AFTER_ACCEPTED; + } else { + sState = STATE_IN_WORD; + } + break; + case STATE_PICKED_SUGGESTION: + if (isSpace) { + sState = STATE_SPACE_AFTER_PICKED; + } else if (isSeparator) { + // Swap + sState = STATE_PUNCTUATION_AFTER_ACCEPTED; + } else { + sState = STATE_IN_WORD; + } + break; + case STATE_START: + case STATE_UNKNOWN: + case STATE_SPACE_AFTER_ACCEPTED: + case STATE_PUNCTUATION_AFTER_ACCEPTED: + case STATE_PUNCTUATION_AFTER_WORD: + if (!isSpace && !isSeparator) { + sState = STATE_IN_WORD; + } else { + sState = STATE_START; + } + break; + case STATE_UNDO_COMMIT: + if (isSpace || isSeparator) { + sState = STATE_ACCEPTED_DEFAULT; + } else { + sState = STATE_IN_WORD; + } + } + } + + public static void backspace() { + if (sState == STATE_ACCEPTED_DEFAULT) { + sState = STATE_UNDO_COMMIT; + sAutoSuggestUndoneCount++; + } else if (sState == STATE_UNDO_COMMIT) { + sState = STATE_IN_WORD; + } + sBackspaceCount++; + } + + public static void reset() { + sState = STATE_START; + } + + public static int getState() { + return sState; + } + + public static void keyPressedAt(Key key, int x, int y) { + if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) { + String out = + "KEY: " + (char) key.codes[0] + + " X: " + x + + " Y: " + y + + " MX: " + (key.x + key.width / 2) + + " MY: " + (key.y + key.height / 2) + + "\n"; + try { + sKeyLocationFile.write(out.getBytes()); + } catch (IOException ioe) { + // TODO: May run out of space + } + } + } +} + diff --git a/src/com/android/inputmethod/latin/Tutorial.java b/src/com/android/inputmethod/latin/Tutorial.java new file mode 100644 index 000000000..2b3138bf9 --- /dev/null +++ b/src/com/android/inputmethod/latin/Tutorial.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.graphics.drawable.Drawable; +import android.opengl.Visibility; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.text.Layout; +import android.text.StaticLayout; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.widget.PopupWindow; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class Tutorial { + + private List mBubbles = new ArrayList(); + private long mStartTime; + private static final long MINIMUM_TIME = 6000; + private static final long MAXIMUM_TIME = 20000; + private View mInputView; + private int[] mLocation = new int[2]; + private int mBubblePointerOffset; + + private static final int MSG_SHOW_BUBBLE = 0; + private static final int MSG_HIDE_ALL = 1; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SHOW_BUBBLE: + Bubble bubba = (Bubble) msg.obj; + bubba.show(mLocation[0], mLocation[1]); + break; + case MSG_HIDE_ALL: + close(true); + } + } + }; + + class Bubble { + Drawable bubbleBackground; + int x; + int y; + int width; + int gravity; + String text; + boolean dismissOnTouch; + boolean dismissOnClose; + PopupWindow window; + TextView textView; + View inputView; + + Bubble(Context context, View inputView, + int backgroundResource, int bx, int by, int bw, int gravity, int textResource, + boolean dismissOnTouch, boolean dismissOnClose) { + bubbleBackground = context.getResources().getDrawable(backgroundResource); + x = bx; + y = by; + width = bw; + this.gravity = gravity; + text = context.getResources().getString(textResource); + this.dismissOnTouch = dismissOnTouch; + this.dismissOnClose = dismissOnClose; + this.inputView = inputView; + window = new PopupWindow(context); + window.setBackgroundDrawable(null); + LayoutInflater inflate = + (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + textView = (TextView) inflate.inflate(R.layout.bubble_text, null); + textView.setBackgroundDrawable(bubbleBackground); + textView.setText(text); + window.setContentView(textView); + window.setFocusable(false); + window.setTouchable(true); + window.setOutsideTouchable(false); + textView.setOnTouchListener(new View.OnTouchListener() { + public boolean onTouch(View view, MotionEvent me) { + Tutorial.this.touched(); + return true; + } + }); + } + + private void chooseSize(PopupWindow pop, View parentView, CharSequence text, TextView tv) { + int wid = tv.getPaddingLeft() + tv.getPaddingRight(); + int ht = tv.getPaddingTop() + tv.getPaddingBottom(); + + /* + * Figure out how big the text would be if we laid it out to the + * full width of this view minus the border. + */ + int cap = width - wid; + + Layout l = new StaticLayout(text, tv.getPaint(), cap, + Layout.Alignment.ALIGN_NORMAL, 1, 0, true); + float max = 0; + for (int i = 0; i < l.getLineCount(); i++) { + max = Math.max(max, l.getLineWidth(i)); + } + + /* + * Now set the popup size to be big enough for the text plus the border. + */ + pop.setWidth(width); + pop.setHeight(ht + l.getHeight()); + } + + void show(int offx, int offy) { + chooseSize(window, inputView, text, textView); + if (inputView.getVisibility() == View.VISIBLE + && inputView.getWindowVisibility() == View.VISIBLE) { + try { + if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) offy -= window.getHeight(); + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) offx -= window.getWidth(); + window.showAtLocation(inputView, Gravity.NO_GRAVITY, x + offx, y + offy); + } catch (Exception e) { + // Input view is not valid + } + } + } + + void hide() { + textView.setOnTouchListener(null); + if (window.isShowing()) { + window.dismiss(); + } + } + } + + public Tutorial(LatinKeyboardView inputView) { + Context context = inputView.getContext(); + int inputHeight = inputView.getHeight(); + int inputWidth = inputView.getWidth(); + mBubblePointerOffset = inputView.getContext().getResources() + .getDimensionPixelOffset(R.dimen.bubble_pointer_offset); + Bubble b0 = new Bubble(context, inputView, + R.drawable.dialog_bubble_step02, 0, 0, + inputWidth, + Gravity.BOTTOM | Gravity.LEFT, + R.string.tip_dismiss, + false, true); + mBubbles.add(b0); + Bubble b1 = new Bubble(context, inputView, + R.drawable.dialog_bubble_step03, + (int) (inputWidth * 0.85) + mBubblePointerOffset, inputHeight / 5, + (int) (inputWidth * 0.45), + Gravity.TOP | Gravity.RIGHT, + R.string.tip_long_press, + true, false); + mBubbles.add(b1); + Bubble b2 = new Bubble(inputView.getContext(), inputView, + R.drawable.dialog_bubble_step04, + inputWidth / 10 - mBubblePointerOffset, inputHeight - inputHeight / 5, + (int) (inputWidth * 0.45), + Gravity.BOTTOM | Gravity.LEFT, + R.string.tip_access_symbols, + true, false); + mBubbles.add(b2); + mInputView = inputView; + } + + void start() { + mInputView.getLocationInWindow(mLocation); + long delayMillis = 0; + for (int i = 0; i < mBubbles.size(); i++) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_BUBBLE, mBubbles.get(i)), delayMillis); + delayMillis += 2000; + } + //mHandler.sendEmptyMessageDelayed(MSG_HIDE_ALL, MAXIMUM_TIME); + mStartTime = SystemClock.uptimeMillis(); + } + + void touched() { + if (SystemClock.uptimeMillis() - mStartTime < MINIMUM_TIME) { + return; + } + for (int i = 0; i < mBubbles.size(); i++) { + Bubble bubba = mBubbles.get(i); + if (bubba.dismissOnTouch) { + bubba.hide(); + } + } + } + + void close(boolean completed) { + mHandler.removeMessages(MSG_SHOW_BUBBLE); + for (int i = 0; i < mBubbles.size(); i++) { + mBubbles.get(i).hide(); + } + if (completed) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences( + mInputView.getContext()); + Editor editor = sp.edit(); + editor.putBoolean(LatinIME.PREF_TUTORIAL_RUN, true); + editor.commit(); + } + } +} diff --git a/src/com/android/inputmethod/latin/UserDictionary.java b/src/com/android/inputmethod/latin/UserDictionary.java new file mode 100644 index 000000000..09549bf8c --- /dev/null +++ b/src/com/android/inputmethod/latin/UserDictionary.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2008 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.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.provider.UserDictionary.Words; + +public class UserDictionary extends Dictionary { + + private static final String[] PROJECTION = { + Words._ID, + Words.WORD, + Words.FREQUENCY + }; + + private static final int INDEX_WORD = 1; + private static final int INDEX_FREQUENCY = 2; + + private static final char QUOTE = '\''; + + private Context mContext; + + List mRoots; + private int mMaxDepth; + private int mInputLength; + + public static final int MAX_WORD_LENGTH = 32; + + private char[] mWordBuilder = new char[MAX_WORD_LENGTH]; + + private ContentObserver mObserver; + + static class Node { + char code; + int frequency; + boolean terminal; + List children; + } + + private boolean mRequiresReload; + + public UserDictionary(Context context) { + mContext = context; + // Perform a managed query. The Activity will handle closing and requerying the cursor + // when needed. + ContentResolver cres = context.getContentResolver(); + + cres.registerContentObserver(Words.CONTENT_URI, true, mObserver = new ContentObserver(null) { + @Override + public void onChange(boolean self) { + mRequiresReload = true; + } + }); + + loadDictionary(); + } + + public synchronized void close() { + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + } + + private synchronized void loadDictionary() { + Cursor cursor = mContext.getContentResolver() + .query(Words.CONTENT_URI, PROJECTION, "(locale IS NULL) or (locale=?)", + new String[] { Locale.getDefault().toString() }, null); + addWords(cursor); + mRequiresReload = false; + } + + /** + * Adds a word to the dictionary and makes it persistent. + * @param word the word to add. If the word is capitalized, then the dictionary will + * recognize it as a capitalized word when searched. + * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered + * the highest. + * @TODO use a higher or float range for frequency + */ + public synchronized void addWord(String word, int frequency) { + if (mRequiresReload) loadDictionary(); + // Safeguard against adding long words. Can cause stack overflow. + if (word.length() >= MAX_WORD_LENGTH) return; + addWordRec(mRoots, word, 0, frequency); + Words.addWord(mContext, word, frequency, Words.LOCALE_TYPE_CURRENT); + // In case the above does a synchronous callback of the change observer + mRequiresReload = false; + } + + @Override + public synchronized void getWords(final WordComposer codes, final WordCallback callback) { + if (mRequiresReload) loadDictionary(); + mInputLength = codes.size(); + mMaxDepth = mInputLength * 3; + getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1.0f, 0, callback); + } + + @Override + public synchronized boolean isValidWord(CharSequence word) { + if (mRequiresReload) loadDictionary(); + return isValidWordRec(mRoots, word, 0, word.length()); + } + + private boolean isValidWordRec(final List children, final CharSequence word, + final int offset, final int length) { + final int count = children.size(); + char currentChar = word.charAt(offset); + for (int j = 0; j < count; j++) { + final Node node = children.get(j); + if (node.code == currentChar) { + if (offset == length - 1) { + if (node.terminal) { + return true; + } + } else { + if (node.children != null) { + if (isValidWordRec(node.children, word, offset + 1, length)) { + return true; + } + } + } + } + } + return false; + } + + static char toLowerCase(char c) { + if (c < BASE_CHARS.length) { + c = BASE_CHARS[c]; + } + c = Character.toLowerCase(c); + return c; + } + + /** + * Recursively traverse the tree for words that match the input. Input consists of + * a list of arrays. Each item in the list is one input character position. An input + * character is actually an array of multiple possible candidates. This function is not + * optimized for speed, assuming that the user dictionary will only be a few hundred words in + * size. + * @param roots node whose children have to be search for matches + * @param codes the input character codes + * @param word the word being composed as a possible match + * @param depth the depth of traversal - the length of the word being composed thus far + * @param completion whether the traversal is now in completion mode - meaning that we've + * exhausted the input and we're looking for all possible suffixes. + * @param snr current weight of the word being formed + * @param inputIndex position in the input characters. This can be off from the depth in + * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type + * "wouldve", it could be matching "would've", so the depth will be one more than the + * inputIndex + * @param callback the callback class for adding a word + */ + private void getWordsRec(List roots, final WordComposer codes, final char[] word, + final int depth, boolean completion, float snr, int inputIndex, + WordCallback callback) { + final int count = roots.size(); + final int codeSize = mInputLength; + // Optimization: Prune out words that are too long compared to how much was typed. + if (depth > mMaxDepth) { + return; + } + int[] currentChars = null; + if (codeSize <= inputIndex) { + completion = true; + } else { + currentChars = codes.getCodesAt(inputIndex); + } + + for (int i = 0; i < count; i++) { + final Node node = roots.get(i); + final char c = node.code; + final char lowerC = toLowerCase(c); + boolean terminal = node.terminal; + List children = node.children; + int freq = node.frequency; + if (completion) { + word[depth] = c; + if (terminal) { + if (!callback.addWord(word, 0, depth + 1, (int) (freq * snr))) { + return; + } + } + if (children != null) { + getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, + callback); + } + } else if (c == QUOTE && currentChars[0] != QUOTE) { + // Skip the ' and continue deeper + word[depth] = QUOTE; + if (children != null) { + getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, + callback); + } + } else { + for (int j = 0; j < currentChars.length; j++) { + float addedAttenuation = (j > 0 ? 1f : 3f); + if (currentChars[j] == -1) { + break; + } + if (currentChars[j] == lowerC || currentChars[j] == c) { + word[depth] = c; + + if (codes.size() == depth + 1) { + if (terminal) { + if (INCLUDE_TYPED_WORD_IF_VALID + || !same(word, depth + 1, codes.getTypedWord())) { + callback.addWord(word, 0, depth + 1, + (int) (freq * snr * addedAttenuation + * FULL_WORD_FREQ_MULTIPLIER)); + } + } + if (children != null) { + getWordsRec(children, codes, word, depth + 1, + true, snr * addedAttenuation, inputIndex + 1, callback); + } + } else if (children != null) { + getWordsRec(children, codes, word, depth + 1, + false, snr * addedAttenuation, inputIndex + 1, callback); + } + } + } + } + } + } + + private void addWords(Cursor cursor) { + mRoots = new ArrayList(); + + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + String word = cursor.getString(INDEX_WORD); + int frequency = cursor.getInt(INDEX_FREQUENCY); + // Safeguard against adding really long words. Stack may overflow due + // to recursion + if (word.length() < MAX_WORD_LENGTH) { + addWordRec(mRoots, word, 0, frequency); + } + cursor.moveToNext(); + } + } + cursor.close(); + } + + private void addWordRec(List children, final String word, + final int depth, final int frequency) { + + final int wordLength = word.length(); + final char c = word.charAt(depth); + // Does children have the current character? + final int childrenLength = children.size(); + Node childNode = null; + boolean found = false; + for (int i = 0; i < childrenLength; i++) { + childNode = children.get(i); + if (childNode.code == c) { + found = true; + break; + } + } + if (!found) { + childNode = new Node(); + childNode.code = c; + children.add(childNode); + } + if (wordLength == depth + 1) { + // Terminate this word + childNode.terminal = true; + childNode.frequency += frequency; // If there are multiple similar words + return; + } + if (childNode.children == null) { + childNode.children = new ArrayList(); + } + addWordRec(childNode.children, word, depth + 1, frequency); + } + + /** + * Table mapping most combined Latin, Greek, and Cyrillic characters + * to their base characters. If c is in range, BASE_CHARS[c] == c + * if c is not a combined character, or the base character if it + * is combined. + */ + static final char BASE_CHARS[] = { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, + 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f, + 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, + 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f, + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, + 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, + 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, + 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, + 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, + 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f, + 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, + 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f, + 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, + 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f, + 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7, + 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020, + 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7, + 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf, + 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043, + 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049, + 0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7, + 0x004f, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00de, 0x0073, // Manually changed d8 to 4f + // Manually changed df to 73 + 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063, + 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, + 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7, + 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, // Manually changed f8 to 6f + 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063, + 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064, + 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065, + 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067, + 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127, + 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, + 0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b, + 0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, + 0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e, + 0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f, + 0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072, + 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073, + 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167, + 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, + 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079, + 0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073, + 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187, + 0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f, + 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197, + 0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f, + 0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7, + 0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055, + 0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7, + 0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf, + 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c, + 0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049, + 0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc, + 0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4, + 0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067, + 0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292, + 0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7, + 0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8, + 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065, + 0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f, + 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075, + 0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068, + 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061, + 0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f, + 0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237, + 0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f, + 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247, + 0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f, + 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257, + 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f, + 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267, + 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f, + 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277, + 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f, + 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287, + 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f, + 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, + 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f, + 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7, + 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af, + 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077, + 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf, + 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7, + 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf, + 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7, + 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df, + 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7, + 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef, + 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7, + 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff, + 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, + 0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f, + 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317, + 0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f, + 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327, + 0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f, + 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337, + 0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f, + 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347, + 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f, + 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357, + 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f, + 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, + 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f, + 0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377, + 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f, + 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7, + 0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9, + 0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, + 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f, + 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7, + 0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9, + 0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7, + 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf, + 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7, + 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf, + 0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7, + 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df, + 0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7, + 0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef, + 0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7, + 0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff, + 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406, + 0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f, + 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, + 0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f, + 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, + 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f, + 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, + 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, + 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, + 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, + 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456, + 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f, + 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467, + 0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f, + 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475, + 0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f, + 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, + 0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f, + 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497, + 0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f, + 0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7, + 0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af, + 0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7, + 0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf, + 0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7, + 0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf, + 0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435, + 0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437, + 0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e, + 0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443, + 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7, + 0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff, + }; + + // generated with: + // cat UnicodeData.txt | perl -e 'while (<>) { @foo = split(/;/); $foo[5] =~ s/<.*> //; $base[hex($foo[0])] = hex($foo[5]);} for ($i = 0; $i < 0x500; $i += 8) { for ($j = $i; $j < $i + 8; $j++) { printf("0x%04x, ", $base[$j] ? $base[$j] : $j)}; print "\n"; }' + +} diff --git a/src/com/android/inputmethod/latin/WordComposer.java b/src/com/android/inputmethod/latin/WordComposer.java new file mode 100644 index 000000000..c950a7f18 --- /dev/null +++ b/src/com/android/inputmethod/latin/WordComposer.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2008-2009 Google Inc. + * + * 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.util.ArrayList; +import java.util.List; + +/** + * A place to store the currently composing word with information such as adjacent key codes as well + */ +public class WordComposer { + /** + * The list of unicode values for each keystroke (including surrounding keys) + */ + private List mCodes; + + /** + * The word chosen from the candidate list, until it is committed. + */ + private String mPreferredWord; + + private StringBuilder mTypedWord; + + /** + * Whether the user chose to capitalize the word. + */ + private boolean mIsCapitalized; + + WordComposer() { + mCodes = new ArrayList(12); + mTypedWord = new StringBuilder(20); + } + + /** + * Clear out the keys registered so far. + */ + public void reset() { + mCodes.clear(); + mIsCapitalized = false; + mPreferredWord = null; + mTypedWord.setLength(0); + } + + /** + * Number of keystrokes in the composing word. + * @return the number of keystrokes + */ + public int size() { + return mCodes.size(); + } + + /** + * Returns the codes at a particular position in the word. + * @param index the position in the word + * @return the unicode for the pressed and surrounding keys + */ + public int[] getCodesAt(int index) { + return mCodes.get(index); + } + + /** + * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of + * the array containing unicode for adjacent keys, sorted by reducing probability/proximity. + * @param codes the array of unicode values + */ + public void add(int primaryCode, int[] codes) { + mTypedWord.append((char) primaryCode); + mCodes.add(codes); + } + + /** + * Delete the last keystroke as a result of hitting backspace. + */ + public void deleteLast() { + mCodes.remove(mCodes.size() - 1); + mTypedWord.deleteCharAt(mTypedWord.length() - 1); + } + + /** + * Returns the word as it was typed, without any correction applied. + * @return the word that was typed so far + */ + public CharSequence getTypedWord() { + int wordSize = mCodes.size(); + if (wordSize == 0) { + return null; + } +// StringBuffer sb = new StringBuffer(wordSize); +// for (int i = 0; i < wordSize; i++) { +// char c = (char) mCodes.get(i)[0]; +// if (i == 0 && mIsCapitalized) { +// c = Character.toUpperCase(c); +// } +// sb.append(c); +// } +// return sb; + return mTypedWord; + } + + public void setCapitalized(boolean capitalized) { + mIsCapitalized = capitalized; + } + + /** + * Whether or not the user typed a capital letter as the first letter in the word + * @return capitalization preference + */ + public boolean isCapitalized() { + return mIsCapitalized; + } + + /** + * Stores the user's selected word, before it is actually committed to the text field. + * @param preferred + */ + public void setPreferredWord(String preferred) { + mPreferredWord = preferred; + } + + /** + * Return the word chosen by the user, or the typed word if no other word was chosen. + * @return the preferred word + */ + public CharSequence getPreferredWord() { + return mPreferredWord != null ? mPreferredWord : getTypedWord(); + } +}