diff --git a/java/proguard.flags b/java/proguard.flags index ca205b927..163352287 100644 --- a/java/proguard.flags +++ b/java/proguard.flags @@ -20,6 +20,10 @@ boolean equalsIgnoreCase(...); } +-keep class com.android.inputmethod.latin.InputPointers { + *; +} + -keep class com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsFragment { *; } @@ -38,6 +42,7 @@ -keep class com.android.inputmethod.latin.ResearchLogger { void flush(); + void publishCurrentLogUnit(...); } -keep class com.android.inputmethod.keyboard.KeyboardLayoutSet$Builder { diff --git a/java/res/values-af/strings-appname.xml b/java/res/values-af/strings-appname.xml new file mode 100644 index 000000000..1adf723d7 --- /dev/null +++ b/java/res/values-af/strings-appname.xml @@ -0,0 +1,28 @@ + + + + + + + "Android-speltoetser" + "Android-sleutelbordinstellings" + "Speltoets tans instellings" + diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml index 2a8a2e8f4..fb7cf349f 100644 --- a/java/res/values-af/strings.xml +++ b/java/res/values-af/strings.xml @@ -20,14 +20,10 @@ - "Android-sleutelbord" "Android-sleutelbord (AOSP)" - "Android-sleutelbordinstellings" "Invoeropsies" "Navorsing-loglêerbevele" - "Android-speltoetser" "Android-speltoetser (AOSP)" - "Speltoetser se instellings" "Soek kontakname op" "Speltoetser gebruik inskrywings uit jou kontaklys" "Vibreer met sleuteldruk" @@ -111,9 +107,19 @@ "let op die tydstempel in die loglêer" "Aangetekende tydstempel" "Moenie hierdie sessie aanteken nie" + + + + "Sessie se loglêer uitgevee" "Sessie se loglêer uitgevee" "Sessie se loglêer NIE uitgevee nie" + + + + + + "Invoertale" "Raak weer om te stoor" "Woordeboek beskikbaar" diff --git a/java/res/values-am/strings-appname.xml b/java/res/values-am/strings-appname.xml new file mode 100644 index 000000000..fd93114f3 --- /dev/null +++ b/java/res/values-am/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "የAndroid ቁልፍ ሰሌዳ" + "Android የፊደል አራሚ" + "Android የቁልፍ ሰሌዳ ቅንብሮች" + "የፊደል አራሚ ቅንብሮች" + diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml index a6373ccb6..d36b9a68b 100644 --- a/java/res/values-am/strings.xml +++ b/java/res/values-am/strings.xml @@ -20,14 +20,10 @@ - "የAndroid ቁልፍሰሌዳ" "የAndroid ቁልፍ ሰሌዳ (AOSP)" - "የAndroid ቁልፍሰሌዳ ቅንብሮች" "ግቤት አማራጮች" "የጥናት የምዝግብ ማስታወሻ ትዕዛዞች" - "Android የፊደል ማረሚያ" "Android የፊደል ማረሚያ (AOSP)" - "የፊደል አራሚ ቅንብሮች" "የእውቅያ ስሞችን ተመልከት" "ፊደል አራሚ ከእውቅያ ዝርዝርህ የገቡትን ይጠቀማል" "በቁልፍመጫንጊዜ አንዝር" @@ -111,9 +107,14 @@ "የምዝግብ ማስታወሻ ጊዜ ማህተም ማስታወሻ" "የጊዜ ማህተም ተመዝግቧል" "ይህን ክፍለ ጊዜ እንዳትመዘግበው" + "ክፍለ ጊዜ ምዝገባን አንቃ" + "ሙሉውን የክፍለጊዜ ታሪክ መዝግብ" "የክፍለጊዜ ምዝግብ ማስታወሻ በመሰረዝ ላይ" "የክፍለ ጊዜ ምዝግብ ማስታወሻ ተሰርዟል" "የክፍለጊዜ ምዝግብ ማስታወሻ አልተሰረዘም" + "የክፍለጊዜ ታሪክ ተመዝግቧል" + "ስህተት፦ክፍለጊዜ ታሪክ አልተመዘገበም" + "ክፍለጊዜ ምዝገባ ነቅቷል" "ቋንቋዎች አግቤት" "ለማስቀመጥ እንደገና ንካ" "መዝገበ ቃላት አለ" diff --git a/java/res/values-ar/strings-appname.xml b/java/res/values-ar/strings-appname.xml new file mode 100644 index 000000000..3d81e5d4b --- /dev/null +++ b/java/res/values-ar/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "لوحة مفاتيح Android" + "التدقيق الإملائي في Android" + "إعدادات لوحة مفاتيح Android" + "إعدادات التدقيق الإملائي" + diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml index 9e6f1ea51..9490704a6 100644 --- a/java/res/values-ar/strings.xml +++ b/java/res/values-ar/strings.xml @@ -20,14 +20,10 @@ - "لوحة مفاتيح Android" "لوحة مفاتيح Android ‏(AOSP)" - "إعدادات لوحة مفاتيح Android" "خيارات الإرسال" "أوامر سجلات البحث" - "التدقيق الإملائي في Android" "التدقيق الإملائي في Android‏ (AOSP)" - "إعدادات التدقيق الإملائي" "بحث في أسماء جهات الاتصال" "يستخدم المدقق الإملائي إدخالات من قائمة جهات الاتصال" "اهتزاز عند ضغط مفتاح" @@ -111,9 +107,14 @@ "ملاحظة طابع زمني في سجل" "تم تسجيل الطابع الزمني" "عدم تسجيل هذه الجلسة" + "تمكين تسجيل الجلسة" + "تسجيل سجل الجلسة بالكامل" "جارٍ حذف سجل الجلسة" "تم حذف سجل الجلسة" "لم يتم حذف سجل الجلسة" + "تم تسجيل سجل الجلسة" + "الخطأ: لم يتم تسجيل سجل الجلسة" + "تم تمكين تسجيل الجلسة" "لغات الإدخال" "المس مرة أخرى للحفظ" "القاموس متاح" diff --git a/java/res/values-be/strings-appname.xml b/java/res/values-be/strings-appname.xml new file mode 100644 index 000000000..e0aadfa3c --- /dev/null +++ b/java/res/values-be/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Клавіятура Android" + "Iнструмент праверкi правапiсу для Android" + "Налады клавіятуры Android" + "Налады праверкі арфаграфіі" + diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml index fb371f4c3..0d563f330 100644 --- a/java/res/values-be/strings.xml +++ b/java/res/values-be/strings.xml @@ -20,14 +20,10 @@ - "Клавіятура Android" "Клавіятура Android (AOSP)" - "Налады клавіятуры Android" "Параметры ўводу" "Каманды гiсторыя даследаванняў" - "Iнструмент праверкi правапiсу для Android" "Iнструмент праверкi правапiсу для Android (AOSP)" - "Налады праверкі арфаграфіі" "Шукаць імёны кантактаў" "Модуль праверкі правапісу выкарыстоўвае запісы са спісу кантактаў" "Вібрацыя пры націску клавіш" @@ -111,9 +107,14 @@ "Пазначыць час у гiсторыi" "Запiсаныя пазнакі" "Не рэгістраваць гэты сеанс" + "Уключыць гiсторыю сеанса" + "Запiс усёй гiсторыi сеанса" "Выдаленне гiсторыi сеанса" "Гiсторыя сеанса выдалена" "Гiсторыя сеанса НЕ выдалена" + "Гiсторыя сеанса запiсана" + "Памылка: гiсторыя сеанса НЕ запiсана" + "Уключаны запiс сеанса" "Мовы ўводу" "Дакраніцеся зноў, каб захаваць" "Слоўнік даступны" diff --git a/java/res/values-bg/strings-appname.xml b/java/res/values-bg/strings-appname.xml new file mode 100644 index 000000000..49e301d32 --- /dev/null +++ b/java/res/values-bg/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Клавиатура на Android" + "Програма за правописна проверка за Android" + "Настройки на клавиатурата на Android" + "Настройки за проверка на правописа" + diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml index 94945c03b..38ebfe84a 100644 --- a/java/res/values-bg/strings.xml +++ b/java/res/values-bg/strings.xml @@ -20,14 +20,10 @@ - "Клавиатура на Android" "Клавиатура на Android (AOSP)" - "Настройки на клавиатурата на Android" "Опции за въвеждане" "Команди за рег. файл за проучвания" - "Програма за правописна проверка за Android" "Програма за правописна проверка за Android (AOSP)" - "Настройки за проверка на правописа" "Търсене на имена" "За проверка на правописа се ползват записи от списъка с контакти" "Да вибрира при натискане на клавиш" @@ -111,9 +107,19 @@ "Отбелязване на часа в рег. файл" "Часът е записан" "Без регистр. на сесията" + + + + "Рег. файл на сесията се изтрива" "Рег. файл на сесията е изтрит" "Рег. файл на сесията НЕ Е изтрит" + + + + + + "Езици за въвеждане" "Докоснете отново, за да запазите" "Има достъп до речник" diff --git a/java/res/values-ca/strings-appname.xml b/java/res/values-ca/strings-appname.xml new file mode 100644 index 000000000..add5c3f2f --- /dev/null +++ b/java/res/values-ca/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Teclat Android" + "Corrector ortogràfic d\'Android" + "Configuració del teclat d\'Android" + "Configuració de la correcció ortogràfica" + diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml index b6fb56093..252441062 100644 --- a/java/res/values-ca/strings.xml +++ b/java/res/values-ca/strings.xml @@ -20,14 +20,10 @@ - "Teclat Android" "Teclat d\'Android (AOSP)" - "Configuració del teclat d\'Android" "Opcions d\'entrada" "Recerca d\'ordres de reg." - "Corrector ortogràfic d\'Android" "Corrector ortogràfic d\'Android (AOSP)" - "Configuració de la correcció ortogràfica" "Cerca noms de contactes" "El corrector ortogràfic utilitza entrades de la llista de cont." "Vibra en prémer tecles" @@ -111,9 +107,14 @@ "Indica m. horària al reg." "Marca horària enregistrada" "No enregistris la sessió" + "Activa el registre de sessió" + "Registra tot l\'historial de sessió" "Suprimint registre de ses." "Registre de ses. suprimit" "Registre de ses. NO sup." + "Historial de sessió registrat" + "Error: historial de sessió NO registrat" + "Registre de sessió activat" "Idiomes d\'entrada" "Torna a tocar per desar" "Diccionari disponible" diff --git a/java/res/values-cs/strings-appname.xml b/java/res/values-cs/strings-appname.xml new file mode 100644 index 000000000..0eeac88b4 --- /dev/null +++ b/java/res/values-cs/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Klávesnice Android" + "Kontrola pravopisu Android" + "Nastavení klávesnice Android" + "Nastavení kontroly pravopisu" + diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml index 5818d5c6f..4697d43ec 100644 --- a/java/res/values-cs/strings.xml +++ b/java/res/values-cs/strings.xml @@ -20,14 +20,10 @@ - "Klávesnice Android" "Klávesnice Android (AOSP)" - "Nastavení klávesnice Android" "Možnosti zadávání textu a dat" "Příkazy vývoj. protokolu" - "Kontrola pravopisu Android" "Kontrola pravopisu Android (AOSP)" - "Nastavení kontroly pravopisu" "Vyhledat kontakty" "Kontrola pravopisu používá záznamy z vašeho seznamu kontaktů." "Při stisku klávesy vibrovat" @@ -111,9 +107,14 @@ "Uložit čas do protokolu" "Časové razítko vloženo" "Neprotokolovat relaci" + "Povolit protokolování relace" + "Protokolovat celou historii relace" "Mazání protokolu relace" "Protokol relace smazán" "Protokol relace nesmazán" + "Historie relace protokolována" + "Historie relace NENÍ protokolována" + "Protokolování relace povoleno" "Vstupní jazyky" "Opětovným dotykem provedete uložení" "Slovník k dispozici" diff --git a/java/res/values-da/strings-appname.xml b/java/res/values-da/strings-appname.xml new file mode 100644 index 000000000..faef5824b --- /dev/null +++ b/java/res/values-da/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android-tastatur" + "Android-stavekontrol" + "Indstillinger for Android-tastatur" + "Indstillinger for stavekontrol" + diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml index dc1df0772..5a128ee58 100644 --- a/java/res/values-da/strings.xml +++ b/java/res/values-da/strings.xml @@ -20,14 +20,10 @@ - "Android-tastatur" "Android-tastatur (AOSP)" - "Android-tastatur-indstillinger" "Indstillinger for input" "Forskningslogkommandoer" - "Android-stavekontrol" "Android-stavekontrol (AOSP)" - "Indstillinger for stavekontrol" "Slå kontaktnavne op" "Stavekontrollen bruger poster fra listen over kontaktpersoner" "Vibration ved tastetryk" @@ -111,9 +107,14 @@ "Notér tidsstempel i log" "Noteret tidsstempel" "Logfør ikke denne session" + "Aktivér logføring af sessioner" + "Logfør hele sessionshistorikken" "Sletter sessionslogfil" "Sessionslogfil slettet" "Sessionslog IKKE slettet" + "Sessionshistorikken er logført" + "Fejl: Sessionshistorik IKKE logført" + "Logføring af sessioner er aktiveret" "Inputsprog" "Tryk igen for at gemme" "Ordbog er tilgængelig" diff --git a/java/res/values-de/strings-appname.xml b/java/res/values-de/strings-appname.xml new file mode 100644 index 000000000..fc5fb8902 --- /dev/null +++ b/java/res/values-de/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android-Tastatur" + "Android-Rechtschreibprüfung" + "Android-Tastatureinstellungen" + "Einstellungen für Rechtschreibprüfung" + diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml index b24952534..9381ecb56 100644 --- a/java/res/values-de/strings.xml +++ b/java/res/values-de/strings.xml @@ -20,14 +20,10 @@ - "Android-Tastatur" "Android-Tastatur (AOSP)" - "Android-Tastatureinstellungen" "Eingabeoptionen" "Forschungsprotokollbefehle" - "Android-Rechtschreibprüfung" "Android-Rechtschreibprüfung (AOSP)" - "Einstellungen für Rechtschreibprüfung" "Kontaktnamen prüfen" "Rechtschreibprüfung verwendet Einträge aus Ihrer Kontaktliste." "Bei Tastendruck vibrieren" @@ -111,9 +107,14 @@ "Zeitstempel im Protokoll" "Zeitstempel aufgenommen" "Nicht protokollieren" + "Sitzungsprotokoll aktivieren" + "Gesamten Sitzungsverlauf speichern" "Protokoll wird gelöscht..." "Protokoll gelöscht" "Protokoll NICHT gelöscht" + "Sitzungsverlauf gespeichert" + "Sitzungsverlauf NICHT gespeichert" + "Sitzungsprotokoll aktiviert" "Eingabesprachen" "Zum Speichern erneut berühren" "Wörterbuch verfügbar" diff --git a/java/res/values-el/strings-appname.xml b/java/res/values-el/strings-appname.xml new file mode 100644 index 000000000..a199655f2 --- /dev/null +++ b/java/res/values-el/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Πληκτρολόγιο Android" + "Ορθογραφικός έλεγχος Android" + "Ρυθμίσεις πληκτρολογίου Android" + "Ρυθμίσεις ορθογραφικού ελέγχου" + diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml index d9e1aa8c4..8125bc4bd 100644 --- a/java/res/values-el/strings.xml +++ b/java/res/values-el/strings.xml @@ -20,14 +20,10 @@ - "Πληκτρολόγιο Android" "Πληκτρολόγιο Android (AOSP)" - "Ρυθμίσεις πληκτρολογίου Android" "Επιλογές εισόδου" "Έρευνα εντολών καταγραφής" - "Ορθογραφικός έλεγχος Android" "Ορθογραφικός έλεγχος Android (AOSP)" - "Ρυθμίσεις ορθογραφικού ελέγχου" "Αναζήτηση ονομάτων επαφών" "Ο ορθογρ. έλεγχος χρησιμοπ. καταχωρίσεις από τη λίστα επαφών σας" "Δόνηση κατά το πάτημα πλήκτρων" @@ -111,9 +107,14 @@ "Χρόνος στο αρχείο καταγρ." "Καταγεγραμμένος χρόνος" "Χωρίς αρχείο καταγραφής" + "Ενεργοποίηση καταγραφής περιόδου" + "Καταγραφή ολόκλ. ιστορικού περιόδου" "Διαγραφή αρχείου σύνδεσης" "Αρχείο καταγρ. διαγράφηκε" "Αρχείο καταγρ. ΔΕΝ διαγρ." + "Το ιστορικό περιόδου καταγράφηκε" + "Σφάλμα: ΜΗ καταγραφή ιστορ. περιόδου" + "Ενεργοποίηση καταγραφής περιόδου" "Γλώσσες εισόδου" "Αγγίξτε ξανά για αποθήκευση" "Λεξικό διαθέσιμο" diff --git a/java/res/values-en-rGB/strings-appname.xml b/java/res/values-en-rGB/strings-appname.xml new file mode 100644 index 000000000..ad9e782b0 --- /dev/null +++ b/java/res/values-en-rGB/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android keyboard" + "Android spell checker" + "Android keyboard settings" + "Spell checking settings" + diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml index 7241e5df0..9697cc792 100644 --- a/java/res/values-en-rGB/strings.xml +++ b/java/res/values-en-rGB/strings.xml @@ -20,14 +20,10 @@ - "Android keyboard" "Android keyboard (AOSP)" - "Android keyboard settings" "Input options" "Research Log Commands" - "Android spell checker" "Android spell checker (AOSP)" - "Spellchecking settings" "Look up contact names" "Spell checker uses entries from your contact list" "Vibrate on key-press" @@ -111,9 +107,14 @@ "Note timestamp in log" "Recorded timestamp" "Do not log this session" + "Enable session logging" + "Log whole session history" "Deleting session log" "Session log deleted" "Session log NOT deleted" + "Session history logged" + "Error: Session history NOT logged" + "Session logging enabled" "Input languages" "Touch again to save" "Dictionary available" diff --git a/java/res/values-es-rUS/strings-appname.xml b/java/res/values-es-rUS/strings-appname.xml new file mode 100644 index 000000000..5f08afba4 --- /dev/null +++ b/java/res/values-es-rUS/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Teclado de Android" + "Corrector ortográfico de Android" + "Configuración de teclado de Android" + "Configuración del corrector ortográfico" + diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml index 01d9912da..00fce612b 100644 --- a/java/res/values-es-rUS/strings.xml +++ b/java/res/values-es-rUS/strings.xml @@ -20,14 +20,10 @@ - "Teclado de Android" "Teclado de Android (AOSP)" - "Configuración de teclado de Android" "Opciones de entrada" "Comandos registro invest." - "Corrector ortográfico de Android" "Corrector ortográfico de Android (AOSP)" - "Configuración del corrector ortográfico" "Buscar nombres contactos" "El corrector ortográfico usa entradas de tu lista de contactos." "Vibrar al pulsar teclas" @@ -111,9 +107,14 @@ "Marcar tiempo en registro" "Marca tiempo registrada" "No registrar esta sesión" + "Activar registro de sesión" + "Registrar his. de sesión completo" "Eliminando registro" "Registro sesión eliminado" "NO se eliminó el registro" + "Se registró el historial de sesión." + "Error al registrar his. de sesión" + "Se activó el historial de sesión." "Idiomas de entrada" "Vuelve a tocar para guardar." "Diccionario disponible" diff --git a/java/res/values-es/strings-appname.xml b/java/res/values-es/strings-appname.xml new file mode 100644 index 000000000..cce9a176d --- /dev/null +++ b/java/res/values-es/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Teclado de Android" + "Corrector ortográfico de Android" + "Ajustes del teclado de Android" + "Ajustes del corrector ortográfico" + diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml index f97d93ddf..7625003c8 100644 --- a/java/res/values-es/strings.xml +++ b/java/res/values-es/strings.xml @@ -20,14 +20,10 @@ - "Teclado de Android" "Teclado Android (AOSP)" - "Ajustes del teclado de Android" "Opciones entrada texto" "Comandos registro investigación" - "Corrector de Android" "Corrector de Android (AOSP)" - "Ajustes del corrector ortográfico" "Nombres de contactos" "Añadir nombres de tu lista de contactos al corrector" "Vibrar al pulsar tecla" @@ -111,9 +107,14 @@ "Anotar marca tiempo en registro" "Marca de tiempo registrada" "No registrar esta sesión" + "Habilitar registro de sesión" + "Registrar historial de sesión" "Eliminando registro..." "Registro eliminado" "Registro no eliminado" + "Historial de sesión registrado" + "Error: historial NO registrado" + "Registro de sesión habilitado" "Idiomas de entrada" "Toca otra vez para guardar." "Hay un diccionario disponible" diff --git a/java/res/values-et/strings-appname.xml b/java/res/values-et/strings-appname.xml new file mode 100644 index 000000000..181d597f9 --- /dev/null +++ b/java/res/values-et/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Androidi klaviatuur" + "Androidi õigekirjakontroll" + "Androidi klaviatuuri seaded" + "Õigekirjakontrolli seaded" + diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml index e4f0f2f58..ae98198ce 100644 --- a/java/res/values-et/strings.xml +++ b/java/res/values-et/strings.xml @@ -20,14 +20,10 @@ - "Androidi klaviatuur" "Android-klaviatuur (AOSP)" - "Androidi klaviatuuriseaded" "Sisestusvalikud" "Uuringulogi käsud" - "Androidi õigekirjakontroll" "Androidi õigekirjakontroll (AOSP)" - "Õigekirjakontrolli seaded" "Kontakti nimede kontroll." "Õigekirjakontroll kasutab teie kontaktisikute loendi sissekandeid" "Vibreeri klahvivajutusel" @@ -111,9 +107,14 @@ "Märgi ajatempel logisse" "Salvestatud ajatemplid" "Ära logi seda seanssi" + "Lubage seansi logimine" + "Logige kogu seansi ajalugu" "Seansi logi kustutamine" "Seansi logi kustutatud" "Seansi logi EI kustutatud" + "Seansi ajalugu on logitud" + "Viga: seansi ajalugu EI OLE logitud" + "Seansi logimine on lubatud" "Sisestuskeeled" "Salvestamiseks puudutage uuesti" "Sõnastik saadaval" diff --git a/java/res/values-fa/strings-appname.xml b/java/res/values-fa/strings-appname.xml new file mode 100644 index 000000000..366d56d92 --- /dev/null +++ b/java/res/values-fa/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "صفحه کلید Android" + "غلط‌گیر املای Android" + "تنظیمات صفحه کلید Android" + "تنظیمات غلط‌‌ گیر املا" + diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml index 1e18a4843..bfdabab04 100644 --- a/java/res/values-fa/strings.xml +++ b/java/res/values-fa/strings.xml @@ -20,14 +20,10 @@ - "صفحه کلید Android" "صفحه کلید (Android (AOSP" - "تنظیمات صفحه کلید Android" "گزینه های ورودی" "فرمان‌های گزارش‌گیری پژوهش" - "غلط‌گیر املای Android" "غلط‌گیر املای Android (AOSP)" - "تنظیمات غلط گیری املایی" "جستجوی نام مخاطبین" "غلط‌گیر املا از ورودی‌های لیست مخاطبین شما استفاده میکند" "لرزش با فشار کلید" @@ -115,9 +111,19 @@ "یادداشت مهر زمان در گزارش" "مهر زمان ثبت شده" "از این جلسه گزارش‌گیری نشود" + + + + "در حال حذف گزارش جلسه" "گزارش جلسه حذف شد" "گزارش جلسه حذف نشد" + + + + + + "زبان‌های ورودی" "برای ذخیره دوباره لمس کنید" "دیکشنری موجود است" diff --git a/java/res/values-fi/strings-appname.xml b/java/res/values-fi/strings-appname.xml new file mode 100644 index 000000000..b2e23d552 --- /dev/null +++ b/java/res/values-fi/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android-näppäimistö" + "Android-oikoluku" + "Android-näppäimistön asetukset" + "Oikolukuasetukset" + diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml index 3ca48be5d..b19b0624e 100644 --- a/java/res/values-fi/strings.xml +++ b/java/res/values-fi/strings.xml @@ -20,14 +20,10 @@ - "Android-näppäimistö" "Android-näppäimistö (AOSP)" - "Android-näppäimistön asetukset" "Syöttövalinnat" "Tutkimuslokin komennot" - "Android-oikoluku" "Android-oikoluku (AOSP)" - "Oikoluvun asetukset" "Hae kontaktien nimiä" "Oikeinkirjoituksen tarkistus käyttää kontaktiluettelosi tietoja." "Käytä värinää näppäimiä painettaessa" @@ -111,9 +107,14 @@ "Merkitse aikaleima lokiin" "Merkitty aikaleima" "Älä tallenna tätä käyttök." + "Ota käyttökertaloki käyttöön" + "Kirjaa koko käyttökerran historia" "Poistetaan lokia" "Käyttökertaloki poistettu" "Lokia EI poistettu" + "Käyttökerran historia kirjattu" + "Virhe: käyttök. historiaa EI kirj." + "Käyttökertaloki käytössä" "Syöttökielet" "Tallenna koskettamalla uudelleen" "Sanakirja saatavilla" diff --git a/java/res/values-fr/strings-appname.xml b/java/res/values-fr/strings-appname.xml new file mode 100644 index 000000000..8e2a6e088 --- /dev/null +++ b/java/res/values-fr/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Clavier Android" + "Correcteur orthographique Android" + "Paramètres du clavier Android" + "Paramètres du correcteur orthographique" + diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml index e457480c1..99c6e0337 100644 --- a/java/res/values-fr/strings.xml +++ b/java/res/values-fr/strings.xml @@ -20,14 +20,10 @@ - "Clavier Android" "Clavier Android (AOSP)" - "Paramètres du clavier Android" "Options de saisie" "Commandes journaux rech." - "Correcteur orthographique Android" "Correcteur orthographique Android (AOSP)" - "Paramètre du correcteur orthographique" "Rechercher noms contacts" "Correcteur orthographique utilise entrées de liste de contacts." "Vibrer à chaque touche" @@ -111,9 +107,14 @@ "Noter heure dans journal" "Heure enregistrée." "Ne pas enregistrer session" + "Activer l\'enregistrement de session" + "Enregistrer historique de la session" "Suppr. journal session…" "Journal session supprimé." "Journal session PAS suppr." + "Historique de la session enregistré." + "Historique session NON enregistré." + "Enregistrement de session activé." "Langues de saisie" "Appuyer de nouveau pour enregistrer" "Dictionnaire disponible" diff --git a/java/res/values-hi/strings-appname.xml b/java/res/values-hi/strings-appname.xml new file mode 100644 index 000000000..02283af9a --- /dev/null +++ b/java/res/values-hi/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android कीबोर्ड" + "Android वर्तनी परीक्षक" + "Android कीबोर्ड सेटिंग" + "वर्तनी जांच सेटिंग" + diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml index 9dfcf614e..81fb684bf 100644 --- a/java/res/values-hi/strings.xml +++ b/java/res/values-hi/strings.xml @@ -20,14 +20,10 @@ - "Android कीबोर्ड" "Android कीबोर्ड (AOSP)" - "Android कीबोर्ड सेटिंग" "इनपुट विकल्‍प" "लॉग आदेशों का शोध करें" - "Android वर्तनी परीक्षक" "Android वर्तनी परीक्षक (AOSP)" - "वर्तनी जांच सेटिंग" "संपर्क नामों को खोजें" "वर्तनी परीक्षक आपकी संपर्क सूची की प्रविष्टियों का उपयोग करता है" "कुंजी दबाने पर कंपन करता है" @@ -111,9 +107,19 @@ "लॉग में टाइमस्‍टैम्‍प नोट करें" "रिकॉर्ड किया गया टाइमस्टैम्प" "इस सत्र को लॉग न करें" + + + + "सत्र लॉग हटाया जा रहा है" "सत्र लॉग हटाया गया" "सत्र लॉग हटाया नहीं गया" + + + + + + "इनपुट भाषाएं" "सहेजने के लिए पुन: स्‍पर्श करें" "शब्‍दकोश उपलब्‍ध है" diff --git a/java/res/values-hr/strings-appname.xml b/java/res/values-hr/strings-appname.xml new file mode 100644 index 000000000..69fa2e9a1 --- /dev/null +++ b/java/res/values-hr/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Androidova tipkovnica" + "Androidova provjera pravopisa" + "Postavke Androidove tipkovnice" + "Postavke provjere pravopisa" + diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml index c8649e951..5a1eefc5e 100644 --- a/java/res/values-hr/strings.xml +++ b/java/res/values-hr/strings.xml @@ -20,14 +20,10 @@ - "Android tipkovnica" "Android tipkovnica (AOSP)" - "Postavke tipkovnice za Android" "Opcije ulaza" "Istraživanje naredbi dnevnika" - "Androidova provjera pravopisa" "Androidova provjera pravopisa (AOSP)" - "Postavke provjere pravopisa" "Potražite imena kontakata" "Provjera pravopisa upotrebljava unose iz vašeg popisa kontakata" "Vibracija pri pritisku na tipku" @@ -111,9 +107,14 @@ "Zabilježi razdoblje u dnevniku" "Zabilježeno razdoblje" "Ne bilježi ovu sesiju" + "Omogući bilježenje sesije" + "Bilježi cijelu povijest sesije" "Brisanje dnevnika sesije" "Izbrisan dnevnik sesije" "Dnevnik sesije NIJE izbrisan" + "Povijest sesije zabilježena je" + "Pogr.: pov. sesije NIJE zabilježena" + "Omogućeno je bilježenje sesije" "Jezici unosa" "Dodirnite ponovo za spremanje" "Rječnik je dostupan" diff --git a/java/res/values-hu/strings-appname.xml b/java/res/values-hu/strings-appname.xml new file mode 100644 index 000000000..ad511cfbc --- /dev/null +++ b/java/res/values-hu/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android-billentyűzet" + "Androidos helyesírás-ellenőrző" + "Android-billentyűzet beállításai" + "A helyesírás-ellenőrzés beállításai" + diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml index d2a38cb24..a0110e49a 100644 --- a/java/res/values-hu/strings.xml +++ b/java/res/values-hu/strings.xml @@ -20,14 +20,10 @@ - "Android-billentyűzet" "Android-billentyűzet (AOSP)" - "Android billentyűzetbeállítások" "Beviteli beállítások" "Naplózási parancsok" - "Androidos helyesírás-ellenőrző" "Androidos helyesírás-ellenőrző (AOSP)" - "Helyesírás-ellenőrzés beállításai" "Névjegyek keresése" "A helyesírás-ellenőrző használja a névjegyek bejegyzéseit" "Rezgés billentyű megnyomása esetén" @@ -111,9 +107,19 @@ "Időbélyegző naplózáskor" "Rögzített időbélyegzők" "Ne naplózza" + + + + "Napló törlése folyamatban" "Napló törölve" "Napló NINCS törölve" + + + + + + "Beviteli nyelvek" "Érintse meg újból a mentéshez" "Van elérhető szótár" diff --git a/java/res/values-in/strings-appname.xml b/java/res/values-in/strings-appname.xml new file mode 100644 index 000000000..283d69247 --- /dev/null +++ b/java/res/values-in/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Keyboard Android" + "Pemeriksa ejaan Android" + "Setelan keyboard Android" + "Setelan pemeriksa ejaan" + diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml index 2d10e6274..b02656c6e 100644 --- a/java/res/values-in/strings.xml +++ b/java/res/values-in/strings.xml @@ -20,14 +20,10 @@ - "Keyboard Android" "Keyboard Android (AOSP)" - "Setelan keyboard Android" "Opsi masukan" "Riset Perintah Log" - "Pemeriksa ejaan Android" "Pemeriksa ejaan Android (AOSP)" - "Setelan pemeriksaan ejaan" "Cari nama kontak" "Pemeriksa ejaan menggunakan entri dari daftar kontak Anda" "Getar jika tombol ditekan" @@ -111,9 +107,14 @@ "Catat cap waktu di log" "Cap waktu yang direkam" "Jangan simpan log sesi ini" + "Aktifkan log sesi" + "Rekam log seluruh riwayat sesi" "Menghapus log sesi" "Log sesi dihapus" "Log sesi BELUM dihapus" + "Log riwayat sesi direkam" + "Ksalahn: Log rwyat sesi TAK direkam" + "Perekaman log sesi diaktifkan" "Bahasa masukan" "Sentuh lagi untuk menyimpan" "Kamus yang tersedia" diff --git a/java/res/values-it/strings-appname.xml b/java/res/values-it/strings-appname.xml new file mode 100644 index 000000000..b84896b9d --- /dev/null +++ b/java/res/values-it/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Tastiera Android" + "Controllo ortografico Android" + "Impostazioni tastiera Android" + "Impostazioni di controllo ortografico" + diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml index c3a2b5135..b23c516e9 100644 --- a/java/res/values-it/strings.xml +++ b/java/res/values-it/strings.xml @@ -20,14 +20,10 @@ - "Tastiera Android" "Tastiera Android (AOSP)" - "Impostazioni tastiera Android" "Opzioni inserimento" "Ricerca comandi di log" - "Controllo ortografico Android" "Controllo ortografico Android (AOSP)" - "Impostazioni di controllo ortografico" "Cerca in nomi contatti" "La funzione di controllo ortografico usa voci dell\'elenco contatti" "Vibrazione tasti" @@ -111,9 +107,14 @@ "Indicazione temporale log" "Indicazione temporale registrata" "Non registrare la sessione" + "Attiva registrazione sessioni" + "Registra intera cronologia sessione" "Eliminazione log sessione" "Log di sessione eliminato" "Log sessione non eliminato" + "Cronologia sessione registrata" + "Errore: cron. sessione NON registr." + "Registrazione sessioni attivata" "Lingue comandi" "Tocca di nuovo per salvare" "Dizionario disponibile" diff --git a/java/res/values-iw/strings-appname.xml b/java/res/values-iw/strings-appname.xml new file mode 100644 index 000000000..f3f4b674c --- /dev/null +++ b/java/res/values-iw/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "מקלדת Android" + "בודק האיות של Android" + "הגדרות מקלדת Android" + "הגדרות בדיקת איות" + diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml index 3859993f9..29bd615b4 100644 --- a/java/res/values-iw/strings.xml +++ b/java/res/values-iw/strings.xml @@ -20,14 +20,10 @@ - "מקלדת Android" "מקלדת Android ‏(AOSP)" - "הגדרות מקלדת של Android" "אפשרויות קלט" "פקודות יומן מחקר" - "בודק האיות של Android" "בודק האיות של Android ‏(AOSP)" - "הגדרות בדיקת איות" "חפש שמות של אנשי קשר" "בודק האיות משתמש בערכים מרשימת אנשי הקשר שלך" "רטט בלחיצה על מקשים" @@ -111,9 +107,14 @@ "ציין חותמת זמן ביומן" "חותמת זמן מתועדת" "אל תרשום הפעלה זו ביומן" + "הפעל רישום הפעלה" + "רשום את כל היסטוריית ההפעלה" "מוחק יומן הפעלה" "יומן הפעלה נמחק" "יומן הפעלה לא נמחק" + "היסטוריית הפעלה נרשמה" + "שגיאה: היסטוריית ההפעלה לא נרשמה" + "רישום הפעלה הופעל" "שפות קלט" "גע שוב כדי לשמור" "מילון זמין" diff --git a/java/res/values-ja/strings-appname.xml b/java/res/values-ja/strings-appname.xml new file mode 100644 index 000000000..16c1c05c6 --- /dev/null +++ b/java/res/values-ja/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Androidキーボード" + "Androidスペルチェッカー" + "Androidキーボードの設定" + "スペルチェックの設定" + diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml index f9349f540..ca13bb9ce 100644 --- a/java/res/values-ja/strings.xml +++ b/java/res/values-ja/strings.xml @@ -20,14 +20,10 @@ - "Androidキーボード" "Androidキーボード(AOSP)" - "Androidキーボードの設定" "入力オプション" "ログコマンドの検索" - "Androidスペルチェッカー" "Androidスペルチェッカー(AOSP)" - "スペルチェックの設定" "連絡先名の検索" "スペルチェッカーでは連絡先リストのエントリを使用します" "キー操作バイブ" @@ -111,9 +107,19 @@ "タイムスタンプを記録" "タイムスタンプ記録済み" "セッションを記録しない" + + + + "セッションログ削除中" "セッションログ削除済み" "セッションログ未削除" + + + + + + "入力言語" "保存するにはもう一度タップ" "辞書を利用できます" diff --git a/java/res/values-ko/strings-appname.xml b/java/res/values-ko/strings-appname.xml new file mode 100644 index 000000000..3d7db6136 --- /dev/null +++ b/java/res/values-ko/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android 키보드" + "Android 맞춤법 검사기" + "Android 키보드 설정" + "맞춤법 검사 설정" + diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml index cd8cb6b51..3126b2e66 100644 --- a/java/res/values-ko/strings.xml +++ b/java/res/values-ko/strings.xml @@ -20,14 +20,10 @@ - "Android 키보드" "Android 키보드(AOSP)" - "Android 키보드 설정" "입력 옵션" "로그 명령 탐색" - "Android 맞춤법 검사기" "Android 맞춤법 검사기(AOSP)" - "맞춤법 검사 설정" "연락처 이름 조회" "맞춤법 검사기가 주소록의 항목을 사용합니다." "키를 누를 때 진동 발생" @@ -111,9 +107,19 @@ "로그에 타임스탬프를 기록" "타임스탬프를 기록함" "이 세션을 로그하지 마세요." + + + + "세션 로그 삭제" "세션 로그가 삭제됨" "세션 로그가 삭제되지 않음" + + + + + + "입력 언어" "저장하려면 다시 터치" "사전 사용 가능" diff --git a/java/res/values-lt/strings-appname.xml b/java/res/values-lt/strings-appname.xml new file mode 100644 index 000000000..668d27531 --- /dev/null +++ b/java/res/values-lt/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "„Android“ klaviatūra" + "„Android“ rašybos tikrinimo programa" + "„Android“ klaviatūros nustatymai" + "Rašybos tikrinimo nustatymai" + diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml index 2f6abc856..7d7b54c8f 100644 --- a/java/res/values-lt/strings.xml +++ b/java/res/values-lt/strings.xml @@ -20,14 +20,10 @@ - "„Android“ klaviatūra" "„Android“ klaviatūra (AOSP)" - "„Android“ klaviatūros nustatymai" "Įvesties parinktys" "Tyrinėti žurnalo komandas" - "„Android“ rašybos tikrinimo programa" "„Android“ rašybos tikrinimo programa (AOSP)" - "Rašybos tikrinimo nustatymai" "Kontaktų vardų paieška" "Rašybos tikrinimo progr. naudoja įrašus, esančius kontaktų sąraše" "Vibruoti, kai paspaudžiami klavišai" @@ -111,9 +107,14 @@ "Pažym. laiko žymę žurnale" "Įrašyta laiko žymė" "Neįrašyti šios sesijos" + "Įgalinti sesijos įrašymą į žurnalą" + "Įrašyti sesijos istoriją į žurnalą" "Ištrinam. sesijos žurnal." "Sesijos žurnalas ištrint." "Sesij. žurnal. NEIŠTRINT." + "Sesijos istorija įrašyta į žurnalą" + "Klaida: sesijos istorija NEĮRAŠYTA" + "Sesijos įrašymas žurnale įgalintas" "Įvesties kalbos" "Jei norite išsaugoti, palieskite dar kartą" "Žodynas galimas" diff --git a/java/res/values-lv/strings-appname.xml b/java/res/values-lv/strings-appname.xml new file mode 100644 index 000000000..e5657a237 --- /dev/null +++ b/java/res/values-lv/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android tastatūra" + "Android pareizrakstības pārbaudītājs" + "Android tastatūras iestatījumi" + "Pareizrakstības pārbaudes iestatījumi" + diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml index 66dd1473e..e5e61e39c 100644 --- a/java/res/values-lv/strings.xml +++ b/java/res/values-lv/strings.xml @@ -20,14 +20,10 @@ - "Android tastatūra" "Android tastatūra (AOSP)" - "Android tastatūras iestatījumi" "Ievades opcijas" "Izpētes žurnāla komandas" - "Android pareizrakstības pārbaudītājs" "Android pareizrakstības pārbaudītājs (AOSP)" - "Pareizrakstības pārbaudes iestatījumi" "Meklēt kontaktp. vārdus" "Pareizrakst. pārbaudītājs lieto ierakstus no kontaktp. saraksta." "Vibrēt, nospiežot taustiņu" @@ -111,9 +107,14 @@ "Atzīmēt laiksp. žurnālā" "Laikspied. ir reģistrēts." "Nereģistrēt šo sesiju" + "Sesijas reģistrēšanas iespējošana" + "Visas sesijas vēstures reģistrēšana" "Not. sesijas žurn. dzēš." "Sesijas žurnāls ir dzēsts" "Sesijas žurn. NAV dzēsts" + "Sesijas vēsture ir reģistrēta." + "Kļūda: sesijas vēsture NAV reģistr." + "Sesijas reģistrēšana ir iespējota." "Ievades valodas" "Pieskarieties vēlreiz, lai saglabātu." "Ir pieejama vārdnīca." diff --git a/java/res/values-ms/strings-appname.xml b/java/res/values-ms/strings-appname.xml new file mode 100644 index 000000000..6273c6595 --- /dev/null +++ b/java/res/values-ms/strings-appname.xml @@ -0,0 +1,28 @@ + + + + + + + "Penyemak ejaan Android" + "Tetapan papan kekunci Android" + "Tetapan penyemakan ejaan" + diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml index 53c902039..cdd23a8de 100644 --- a/java/res/values-ms/strings.xml +++ b/java/res/values-ms/strings.xml @@ -20,14 +20,10 @@ - "Papan kekunci Android" "Papan kekunci Android (AOSP)" - "Tetapan papan kekunci Android" "Pilihan input" "Arahan Log Penyelidikan" - "Penyemak ejaan Android" "Penyemak ejaan Android (AOSP)" - "Tetapan penyemakan ejaan" "Cari nama kenalan" "Penyemak ejaan menggunakan entri dari senarai kenalan anda" "Getar pada tekanan kekunci" @@ -111,9 +107,19 @@ "Tanda cap waktu dalam log" "Cap waktu direkodkan" "Jangan log sesi ini" + + + + "Memadam log sesi" "Log sesi dipadam" "Log sesi TIDAK dipadam" + + + + + + "Bahasa input" "Sentuh lagi untuk menyimpan" "Kamus tersedia" diff --git a/java/res/values-nb/strings-appname.xml b/java/res/values-nb/strings-appname.xml new file mode 100644 index 000000000..56c1c3c71 --- /dev/null +++ b/java/res/values-nb/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android-tastatur" + "Android-stavekontroll" + "Innstillinger for Android-tastatur" + "Innstillinger for stavekontroll" + diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml index bdde36ae8..5f7519a8d 100644 --- a/java/res/values-nb/strings.xml +++ b/java/res/values-nb/strings.xml @@ -20,14 +20,10 @@ - "Skjermtastatur" "Android-tastatur (AOSP)" - "Innstillinger for skjermtastatur" "Inndataalternativer" "Kommandoer for undersøkelseslogging" - "Android-stavekontroll" "Android-stavekontroll (AOSP)" - "Innstillinger for stavekontroll" "Slå opp kontaktnavn" "Stavekontrollen bruker oppføringer fra kontaktlisten din" "Vibrer ved tastetrykk" @@ -111,9 +107,14 @@ "Notér tidsstempel i logg" "Registrerte tidsstempel" "Ikke loggfør denne økten" + "Aktiver lagring av økter" + "Lagre hele øktloggen" "Sletter øktloggen" "Øktloggen ble slettet" "Øktloggen ble IKKE slettet" + "Øktloggen er lagret" + "Feil: Øktloggen er IKKE lagret" + "Øktlagring er aktivert" "Inndataspråk" "Trykk på nytt for å lagre" "Ordbok tilgjengelig" diff --git a/java/res/values-nl/strings-appname.xml b/java/res/values-nl/strings-appname.xml new file mode 100644 index 000000000..ee288efbb --- /dev/null +++ b/java/res/values-nl/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android-toetsenbord" + "Spellingcontrole van Android" + "Instellingen voor Android-toetsenbord" + "Instellingen voor spellingcontrole" + diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml index 9624e176e..19c9eda1c 100644 --- a/java/res/values-nl/strings.xml +++ b/java/res/values-nl/strings.xml @@ -20,14 +20,10 @@ - "Android-toetsenbord" "Android-toetsenbord (AOSP)" - "Instellingen voor Android-toetsenbord" "Invoeropties" "Opdrachten in onderzoekslogbestand" - "Spellingcontrole van Android" "Spellingcontrole van Android (AOSP)" - "Instellingen voor spellingcontrole" "Contactnamen opzoeken" "De spellingcontrole gebruikt items uit uw contactenlijst" "Trillen bij toetsaanslag" @@ -111,9 +107,14 @@ "Tijdstempel opnemen in logbestand" "Opgenomen tijdstempel" "Sessie niet registreren" + "Sessieregistratie inschakelen" + "Hele sessiegeschiedenis registreren" "Sessielogbestand verwijderen" "Sessielogbestand verwijderd" "Sessielogbestand NIET verwijderd" + "Sessiegeschiedenis geregistreerd" + "Fout: sessiegesch. NIET geregistr." + "Sessieregistratie ingeschakeld" "Invoertalen" "Raak nogmaals aan om op te slaan" "Woordenboek beschikbaar" diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml index c8a72b77a..6ffe8dce3 100644 --- a/java/res/values-pl/strings.xml +++ b/java/res/values-pl/strings.xml @@ -20,14 +20,10 @@ - "Klawiatura Android" "Klawiatura Androida (AOSP)" - "Ustawienia klawiatury Android" "Opcje wprowadzania" "Polecenia dziennika badań" - "Słownik Androida" "Sprawdzanie pisowni na Androidzie (AOSP)" - "Ustawienia sprawdzania pisowni" "Przeszukaj kontakty" "Sprawdzanie pisowni bierze pod uwagę wpisy z listy kontaktów." "Wibracja przy naciśnięciu" @@ -111,9 +107,19 @@ "Znacznik czasu uwagi w dzienniku" "Zapisano znacznik czasu" "Nie rejestruj tej sesji" + + + + "Usuwanie dziennika sesji" "Usunięto dziennik sesji" "Dziennik sesji NIEUSUNIĘTY" + + + + + + "Języki wprowadzania" "Dotknij ponownie, aby zapisać" "Słownik dostępny" diff --git a/java/res/values-pt-rPT/strings-appname.xml b/java/res/values-pt-rPT/strings-appname.xml new file mode 100644 index 000000000..1b88acb69 --- /dev/null +++ b/java/res/values-pt-rPT/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Teclado do Android" + "Verificador ortográfico do Android" + "Definições de teclado do Android" + "Definições da verificação ortográfica" + diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml index cef2ce75b..f16070bb4 100644 --- a/java/res/values-pt-rPT/strings.xml +++ b/java/res/values-pt-rPT/strings.xml @@ -20,14 +20,10 @@ - "Teclado do Android" "Teclado Android (AOSP)" - "Definições de teclado do Android" "Opções de introdução" "Comandos de Reg. Invest." - "Verificador ortográfico do Android" "Verificador ortográfico do Android (AOSP)" - "Definições da verificação ortográfica" "Procurar nomes de contac." "O corretor ortográfico utiliza entradas da sua lista de contactos" "Vibrar ao primir as teclas" @@ -111,9 +107,14 @@ "Anotar car. data no reg." "Carimbo de data gravado" "Não registar esta sessão" + "Ativar registos de sessão" + "Registar hist. de sessões completo" "A eliminar reg. da sessão" "Reg. de sessão eliminado" "Reg. de sessão NÃO elim." + "Histórico de sessões registado" + "Erro: hist. de sessões NÃO regist." + "Registo de sessões ativado" "Idiomas de introdução" "Toque novamente para guardar" "Dicionário disponível" diff --git a/java/res/values-pt/strings-appname.xml b/java/res/values-pt/strings-appname.xml new file mode 100644 index 000000000..3987a6dea --- /dev/null +++ b/java/res/values-pt/strings-appname.xml @@ -0,0 +1,28 @@ + + + + + + + "Corretor ortográfico do Android" + "Configurações de teclado do Android" + "Configurações de verificação ortográfica" + diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml index d092879cf..93666ac3b 100644 --- a/java/res/values-pt/strings.xml +++ b/java/res/values-pt/strings.xml @@ -20,14 +20,10 @@ - "Teclado Android" "Teclado Android (AOSP)" - "Configurações de teclado Android" "Opções de entrada" "Pesq. comandos de reg." - "Corretor ortográfico do Android" "Corretor ortográfico do Android (AOSP)" - "Configurações de verificação ortográfica" "Buscar nomes de contatos" "O corretor ortográfico usa entradas de sua lista de contatos" "Vibrar ao tocar a tecla" @@ -111,9 +107,19 @@ "Indicar data/hora no reg." "Data/hora registrada" "Não registrar esta sessão" + + + + "Excluindo reg. de sessão" "Registro excluído" "Registro NÃO excluído" + + + + + + "Idiomas de entrada" "Toque novamente para salvar" "Dicionário disponível" diff --git a/java/res/values-rm/strings.xml b/java/res/values-rm/strings.xml index b39691ceb..670ce57f1 100644 --- a/java/res/values-rm/strings.xml +++ b/java/res/values-rm/strings.xml @@ -20,20 +20,14 @@ - "Tastatura Android" - "Parameters da la tastatura Android" - - - - @@ -190,12 +184,22 @@ + + + + + + + + + + diff --git a/java/res/values-ro/strings-appname.xml b/java/res/values-ro/strings-appname.xml new file mode 100644 index 000000000..dfa642204 --- /dev/null +++ b/java/res/values-ro/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Tastatură Android" + "Verificator ortografic Android" + "Setările tastaturii Android" + "Setările de verificare ortografică" + diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml index 08275665c..620a5c4a0 100644 --- a/java/res/values-ro/strings.xml +++ b/java/res/values-ro/strings.xml @@ -20,14 +20,10 @@ - "Tastatură Android" "Tastatură Android (AOSP)" - "Setările tastaturii Android" "Opţiuni de introducere text" "Comenzi jurnal cercetare" - "Verificator ortografic Android" "Verificator ortografic Android (AOSP)" - "Setări de verificare ortografică" "Verificare nume în agendă" "Verificatorul ortografic utilizează intrări din lista de contacte" "Vibrare la apăsarea tastei" @@ -111,9 +107,19 @@ "Înreg. marc. temp. jurnal" "Marcaj temporal înregis." "Nu înregistraţi sesiunea" + + + + "Se șterge jurnal sesiune" "Jurnal de sesiune șters" "Jurnal sesiune neşters" + + + + + + "Limbi de intrare" "Atingeţi din nou pentru a salva" "Dicţionar disponibil" diff --git a/java/res/values-ru/strings-appname.xml b/java/res/values-ru/strings-appname.xml new file mode 100644 index 000000000..5db1d0bc9 --- /dev/null +++ b/java/res/values-ru/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Клавиатура Android" + "Проверка правописания Android" + "Настройки клавиатуры Android" + "Настройки проверки правописания" + diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml index 77ffe7b55..c50d1dee1 100644 --- a/java/res/values-ru/strings.xml +++ b/java/res/values-ru/strings.xml @@ -20,14 +20,10 @@ - "Клавиатура Android" "Клавиатура Android (AOSP)" - "Клавиатура Android" "Настройки" "Все команды" - "Проверка правописания Android" "Проверка правописания Android (AOSP)" - "Настройка проверки правописания" "Поиск контактов" "Обращаться к списку контактов при проверке правописания" "Виброотклик клавиш" @@ -111,9 +107,19 @@ "Закладка в журнале" "Закладка сохранена" "Не сохранять этот сеанс" + + + + "Удаление…" "Запись сеанса удалена" "Запись сеанса НЕ удалена" + + + + + + "Языки ввода" "Нажмите, чтобы сохранить" "Доступен словарь" diff --git a/java/res/values-sk/strings-appname.xml b/java/res/values-sk/strings-appname.xml new file mode 100644 index 000000000..5b5590000 --- /dev/null +++ b/java/res/values-sk/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Klávesnica Android" + "Kontrola pravopisu Android" + "Nastavenia klávesnice Android" + "Nastavenia kontroly pravopisu" + diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml index 2b31bd858..d89d1d3b6 100644 --- a/java/res/values-sk/strings.xml +++ b/java/res/values-sk/strings.xml @@ -20,14 +20,10 @@ - "Klávesnica Android" "Klávesnica Android (AOSP)" - "Nastavenia klávesnice Android" "Možnosti zadávania textu a údajov" "Príkazy denníka výskumu" - "Kontrola pravopisu Android" "Kontrola pravopisu Android (AOSP)" - "Nastavenia kontroly pravopisu" "Vyhľadať kontakty" "Kontrola pravopisu používa záznamy z vášho zoznamu kontaktov" "Pri stlačení klávesu vibrovať" @@ -111,9 +107,19 @@ "Časová pečiatka denníka" "Časová pečiatka zaznamenaná" "Neukl. reláciu do denníka" + + + + "Odstraň. denníka relácie" "Denník relácie odstránený" "Denník relácie NIE JE odstr." + + + + + + "Jazyky vstupu" "Opätovným dotykom uložíte" "K dispozícii je slovník" diff --git a/java/res/values-sl/strings-appname.xml b/java/res/values-sl/strings-appname.xml new file mode 100644 index 000000000..fd303d8dd --- /dev/null +++ b/java/res/values-sl/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Tipkovnica Android" + "Črkovalnik za Android" + "Nastavitve tipkovnice Android" + "Nastavitve preverjanja črkovanja" + diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml index 05e20b04c..ad3e7395f 100644 --- a/java/res/values-sl/strings.xml +++ b/java/res/values-sl/strings.xml @@ -20,14 +20,10 @@ - "Tipkovnica Android" "Tipkovnica Android (AOSP)" - "Nastavitve tipkovnice Android" "Možnosti vnosa" "Ukazi za dnevnik raziskav" - "Črkovalnik za Android" "Črkovalnik za Android (AOSP)" - "Nastavitve preverjanja črkovanja" "Iskanje imen stikov" "Črkovalnik uporablja vnose s seznama stikov" "Vibriranje ob pritisku tipke" @@ -111,9 +107,14 @@ "V dnev. zabeleži čas. žig" "Časovni žig zabeležen" "Brez dnevnika za to sejo" + "Omogoči zapisovanje seje v dnevnik" + "Zapiši celotno zgodovino seje v dnevnik" "Brisanje seje dnevnika" "Dnevnik seje izbrisan" "Dnevnik seje NI izbrisan" + "Zgodovina seje zapisana v dnevnik" + "Napaka: zgodovina seje NI zapisana v dnevnik" + "Zapisovanje seje v dnevnik omogočeno" "Jeziki vnosa" "Dotaknite se še enkrat, da shranite" "Slovar je na voljo" diff --git a/java/res/values-sr/strings-appname.xml b/java/res/values-sr/strings-appname.xml new file mode 100644 index 000000000..449fe551a --- /dev/null +++ b/java/res/values-sr/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android тастатура" + "Android провера правописа" + "Подешавања Android тастатуре" + "Подешавања провере правописа" + diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml index 43fe7003d..d1c440d07 100644 --- a/java/res/values-sr/strings.xml +++ b/java/res/values-sr/strings.xml @@ -20,14 +20,10 @@ - "Android тастатура" "Android тастатура (AOSP)" - "Подешавања Android тастатуре" "Опције уноса" "Команде евиденције истраживања" - "Android провера правописа" "Android провера правописа (AOSP)" - "Подешавања провере правописа" "Потражи имена контаката" "Контролор правописа користи уносе са листе контаката" "Вибрирај на притисак тастера" @@ -111,9 +107,14 @@ "Наведи временску ознаку у евиденцији" "Снимљена временска ознака" "Не евидентирај ову сесију" + "Омогући евидентирање сесија" + "Евидентирај целу историју сесија" "Брисање евиденције сесије" "Евиденција сесије је обрисана" "Евиденција сесије НИЈЕ избрисана" + "Историја сесија је евидентирана" + "Грешка: Историја сесија НИЈЕ евидентирана" + "Евидентирање сесија је омогућено" "Језици уноса" "Поново додирните да бисте сачували" "Речник је доступан" diff --git a/java/res/values-sv/strings-appname.xml b/java/res/values-sv/strings-appname.xml new file mode 100644 index 000000000..9b4a7dbd1 --- /dev/null +++ b/java/res/values-sv/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Androids tangentbord" + "Stavningskontroll i Android" + "Inställningar för Androids tangentbord" + "Inställningar för stavningskontroll" + diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml index f24446976..0da487723 100644 --- a/java/res/values-sv/strings.xml +++ b/java/res/values-sv/strings.xml @@ -20,14 +20,10 @@ - "Androids tangentbord" "Androids tangentbord (AOSP)" - "Inställningar för Androids tangentbord" "Inmatningsalternativ" "Loggkommandon" - "Stavningskontroll i Android" "Stavningskontroll i Android (AOSP)" - "Inställningar för stavningskontroll" "Sök namn på kontakter" "I stavningskontrollen används poster från kontaktlistan" "Vibrera vid tangenttryck" @@ -111,9 +107,14 @@ "Markera tidpunkt i loggen" "Tidpunkten har sparats" "Logga inte detta besök" + "Aktivera sessionsloggning" + "Logga hela sessionshistoriken" "Besöksloggen tas bort" "Besöksloggen togs bort" "Besöksloggen togs EJ bort" + "Sessionshistoriken har loggats" + "Fel: historiken har INTE loggats" + "Sessionsloggning är aktiverat" "Inmatningsspråk" "Spara genom att trycka igen" "En ordlista är tillgänglig" diff --git a/java/res/values-sw/strings-appname.xml b/java/res/values-sw/strings-appname.xml new file mode 100644 index 000000000..51de0a6b8 --- /dev/null +++ b/java/res/values-sw/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Kibodi ya Android" + "Kikagua tahajia cha Android" + "Mipangilio ya kibodi ya Android" + "Mipangilio ya kukagua tahajia" + diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml index 9cec8679d..545c41157 100644 --- a/java/res/values-sw/strings.xml +++ b/java/res/values-sw/strings.xml @@ -20,14 +20,10 @@ - "Kibodi ya Android" "Kicharazio cha Android (AOSP)" - "Mipangilio ya kibodi ya Android" "Chaguo za uingizaji" "Amri za Kumbukumbu za Utafiti" - "Kikagua tahajia cha Android" "Kikagua tahajia cha Android (AOSP)" - "Mipangilio ya kukagua sarufi" "Angalia majina ya wasiliani" "Kikagua tahajia hutumia ingizo kutoka kwa orodha yako ya anwani" "Tetema unabofya kitufe" @@ -111,9 +107,14 @@ "Dokeza mhuri wa muda kwenye kumbukumbu" "Mhuri wa muda uliorekodiwa" "Usihifadhi kumbukumbu za kipindi hiki" + "Wezesha kuingia kwenye kipindi" + "Ingia kwenye historia ya kipindi kizima" "Inafuta kumbukumbu za kipindi" "Kumbukumbu za kipindi zimefutwa" "Kumbukumbu za kipindi HAZIJAFUTWA" + "Historia ya kipindi imeingia" + "Hitilafu: Historia ya kipindi HAIJAINGIA" + "Kuingia kwa kipindi kumewezeshwa" "Lugha zinazoruhusiwa" "Gusa tena ili kuhifadhi" "Kamusi inapatikana" diff --git a/java/res/values-th/strings-appname.xml b/java/res/values-th/strings-appname.xml new file mode 100644 index 000000000..87205d0eb --- /dev/null +++ b/java/res/values-th/strings-appname.xml @@ -0,0 +1,30 @@ + + + + + "แป้นพิมพ์แอนดรอยด์" + + + + + + + diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml index 174ee4507..c47fc0597 100644 --- a/java/res/values-th/strings.xml +++ b/java/res/values-th/strings.xml @@ -20,14 +20,10 @@ - "แป้นพิมพ์ Android" "Android keyboard (AOSP)" - "การตั้งค่าแป้นพิมพ์ Android" "ตัวเลือกการป้อนข้อมูล" "คำสั่งบันทึกการวิจัย" - "แอนดรอยด์ตรวจสอบการสะกด" "แอนดรอยด์ตรวจสอบการสะกด (AOSP)" - "การตั้งค่าการตรวจสอบการสะกด" "ค้นหารายชื่อติดต่อ" "เครื่องมือตรวจการสะกดใช้รายการจากรายชื่อติดต่อของคุณ" "สั่นเมื่อกดปุ่ม" @@ -111,9 +107,19 @@ "จดเวลาบันทึกไว้ในบันทึก" "บันทึกเวลาบันทึกแล้ว" "อย่าบันทึกเซสชันนี้" + + + + "กำลังลบบันทึกเซสชัน" "ลบบันทึกเซสชันแล้ว" "บันทึกเซสชันไม่ถูกลบ" + + + + + + "ภาษาสำหรับการป้อนข้อมูล" "แตะอีกครั้งเพื่อบัน​​ทึก" "มีพจนานุกรมให้ใช้งาน" diff --git a/java/res/values-tl/strings-appname.xml b/java/res/values-tl/strings-appname.xml new file mode 100644 index 000000000..fd2b3f55b --- /dev/null +++ b/java/res/values-tl/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Keyboard ng Android" + "Spell checker ng Android" + "Mga setting ng keyboard ng Android" + "Mga setting ng pag-spell check" + diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml index 25801ca7d..766972f46 100644 --- a/java/res/values-tl/strings.xml +++ b/java/res/values-tl/strings.xml @@ -20,14 +20,10 @@ - "Android keyboard" "Android keyboard (AOSP)" - "Mga setting ng Android keyboard" "Mga pagpipilian sa input" "Cmmnd sa Log ng Pnnliksik" - "Pang-check ng pagbabaybay ng Android" "Pang-check ng pagbabaybay ng Android (AOSP)" - "Mga setting ng pang-check ng pagbabaybay" "Maghanap pangalan contact" "Gumagamit pang-check pagbabaybay entry sa iyong listahan contact" "Mag-vibrate sa keypress" @@ -111,9 +107,19 @@ "Tandaan timestamp sa log" "Na-record na timestamp" "Huwag i-log ang session" + + + + "Tinatanggl log ng session" "Tinanggal log ng session" "HND ntnggl log ng session" + + + + + + "Mga wika ng input" "Pinduting muli upang i-save" "Available ang diksyunaryo" diff --git a/java/res/values-tr/strings-appname.xml b/java/res/values-tr/strings-appname.xml new file mode 100644 index 000000000..f5e36d2e8 --- /dev/null +++ b/java/res/values-tr/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android klavyesi" + "Android yazım denetleyici" + "Android klavye ayarları" + "Yazım denetimi ayarları" + diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml index 190736e82..0f5857ab3 100644 --- a/java/res/values-tr/strings.xml +++ b/java/res/values-tr/strings.xml @@ -20,14 +20,10 @@ - "Android klavyesi" "Android klavye (AOSP)" - "Android klavye ayarları" "Giriş seçenekleri" "Araştırma Günlüğü Komutları" - "Android yazım denetleyici" "Android yazım denetleyici (AOSP)" - "Yazım denetimi ayarları" "Kişi adlarını denetle" "Yazım denetleyici, kişi listenizdeki girişleri kullanır" "Tuşa basıldığında titret" @@ -111,9 +107,19 @@ "Günlüğe zaman damgası koy" "Zaman damgası kaydedildi" "Bu oturumu günlüğe kaydetme" + + + + "Oturum günlüğü siliniyor" "Oturum günlüğü silindi" "Oturum günlüğü SİLİNMEDİ" + + + + + + "Giriş dilleri" "Kaydetmek için tekrar dokunun" "Sözlük kullanılabilir" diff --git a/java/res/values-uk/strings-appname.xml b/java/res/values-uk/strings-appname.xml new file mode 100644 index 000000000..fdbb89fd9 --- /dev/null +++ b/java/res/values-uk/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Клавіатура Android" + "Засіб перевірки орфографії Android" + "Налаштування клавіатури Android" + "Налаштування перевірки орфографії" + diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml index 04a8a638e..d2f9091f6 100644 --- a/java/res/values-uk/strings.xml +++ b/java/res/values-uk/strings.xml @@ -20,14 +20,10 @@ - "Клавіатура Android" "Клавіатура Android (AOSP)" - "Налашт-ня клавіат. Android" "Парам. введення" "Команди журналу дослідж." - "Засіб перевірки орфографії Android" "Засіб перевірки орфографії Android (AOSP)" - "Налаштування перевірки орфографії" "Шукати імена контактів" "Програма перевірки правопису використ. записи зі списку контактів" "Вібр. при натисканні клавіш" @@ -111,9 +107,14 @@ "Мітка часу в журналі" "Записана мітка часу" "Не реєструвати цю сесію" + "Увімкнути запис журналу сеансу" + "Записувати історію всього сеансу" "Видалення журналу сесії" "Журнал сесії видалено" "Журнал сесії НЕ видалено" + "Історію сеансу записано" + "Помилка. Історію сеансу НЕ записано" + "Запис журналу сеансу ввімкнено" "Мови введення" "Торкніться знову, щоб зберегти" "Словник доступний" diff --git a/java/res/values-vi/strings-appname.xml b/java/res/values-vi/strings-appname.xml new file mode 100644 index 000000000..6e32d0370 --- /dev/null +++ b/java/res/values-vi/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Bàn phím Android" + "Trình kiểm tra chính tả Android" + "Cài đặt bàn phím Android" + "Cài đặt kiểm tra chính tả" + diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml index c9dc0e644..789d4da48 100644 --- a/java/res/values-vi/strings.xml +++ b/java/res/values-vi/strings.xml @@ -20,14 +20,10 @@ - "Bàn phím Android" "Bàn phím Android (AOSP)" - "Cài đặt bàn phím Android" "Tùy chọn nhập" "Lệnh ghi nhật ký cho nghiên cứu" - "Trình kiểm tra chính tả Android" "Trình kiểm tra chính tả Android (AOSP)" - "Cài đặt kiểm tra chính tả" "Tra cứu tên liên hệ" "Trình kiểm tra chính tả sử dụng các mục nhập từ danh sách liên hệ của bạn" "Rung khi nhấn phím" @@ -111,9 +107,19 @@ "Dấu thời gian ghi chú trong nhật ký" "Dấu thời gian đã ghi" "Không ghi nhật ký phiên này" + + + + "Đang xóa nhật ký phiên" "Đã xóa nhật ký phiên" "Nhật ký phiên KHÔNG bị xóa" + + + + + + "Ngôn ngữ nhập" "Chạm lại để lưu" "Có sẵn từ điển" diff --git a/java/res/values-zh-rCN/strings-appname.xml b/java/res/values-zh-rCN/strings-appname.xml new file mode 100644 index 000000000..2c1064ad6 --- /dev/null +++ b/java/res/values-zh-rCN/strings-appname.xml @@ -0,0 +1,28 @@ + + + + + + + "Android 拼写检查工具" + "Android 键盘设置" + "拼写检查设置" + diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml index 9f5e4633d..ef1c8102e 100644 --- a/java/res/values-zh-rCN/strings.xml +++ b/java/res/values-zh-rCN/strings.xml @@ -20,14 +20,10 @@ - "Android 键盘" "Android 键盘 (AOSP)" - "Android 键盘设置" "输入选项" "研究记录命令" - "Android 拼写检查工具" "Android 拼写检查工具 (AOSP)" - "拼写检查设置" "查找联系人姓名" "拼写检查工具会使用您的联系人列表中的条目" "按键振动" @@ -111,9 +107,19 @@ "标记记录中的时间" "已标记时间" "不记录本次会话" + + + + "正在删除会话记录" "会话记录已删除" "未能删除会话记录" + + + + + + "输入语言" "再次触摸即可保存" "有可用词典" diff --git a/java/res/values-zh-rTW/strings-appname.xml b/java/res/values-zh-rTW/strings-appname.xml new file mode 100644 index 000000000..8cc663826 --- /dev/null +++ b/java/res/values-zh-rTW/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Android 鍵盤" + "Android 拼字檢查" + "Android 鍵盤設定" + "拼字檢查設定" + diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml index 0cc8a9d82..20c40eb93 100644 --- a/java/res/values-zh-rTW/strings.xml +++ b/java/res/values-zh-rTW/strings.xml @@ -20,14 +20,10 @@ - "Android 鍵盤" "Android 鍵盤 (AOSP)" - "Android 鍵盤設定" "輸入選項" "研究紀錄指令" - "Android 拼字檢查" "Android 拼字檢查 (AOSP)" - "拼字檢查設定" "查詢聯絡人姓名" "拼字檢查程式使用您的聯絡人清單項目" "按鍵時震動" @@ -111,9 +107,19 @@ "在紀錄中加註時間戳記" "已記錄時間戳記" "不要記錄這個工作階段" + + + + "正在刪除工作階段紀錄" "已刪除工作階段紀錄" "「未」刪除工作階段紀錄" + + + + + + "輸入語言" "再次輕觸即可儲存" "可使用字典" diff --git a/java/res/values-zu/strings-appname.xml b/java/res/values-zu/strings-appname.xml new file mode 100644 index 000000000..a0fb51716 --- /dev/null +++ b/java/res/values-zu/strings-appname.xml @@ -0,0 +1,27 @@ + + + + + "Ikhibhodi ye-Android" + "Isihloli sokupela se-Android" + "Izilungiselelo zekhibhodi ye-Android" + "Izilungiselelo zokuhlola ukupela" + diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml index 35cb99cf8..da222092e 100644 --- a/java/res/values-zu/strings.xml +++ b/java/res/values-zu/strings.xml @@ -20,14 +20,10 @@ - "Ikhibhodi ye-Android" "Ikhibhodi ye-Android (AOSP)" - "Izilungiselelo zekhibhodi ye-Android" "Okukhethwa kukho kokungenayo" "Imiyalo yefayela lokungena lokucwaninga" - "Isihloli sokupela se-Android" "Isihloli sokupela se-Android (AOSP)" - "Izilungiselelo zokuhlola ukupela" "Bheka amagama woxhumana nabo" "Isihloli sokupela sisebenzisa okungenayo kusuka kuhlu lalabo oxhumana nabo" "Dlidlizelisa ngokucindezela inkinobho" @@ -111,9 +107,14 @@ "Qaphela isitembu sesikhathi efayeleni lokungena" "Isitembu sesikhathi esirekhodiwe" "Ungenzi ifayela lokungena lalesi sikhathi" + "Nika amandla ukungena ngemvume kwesikhathi" + "Umlando wesikhathi sonke sefayela lokungena" "Isusa ifayela lokungena lesikhathi" "Ifayela lokungena lesikhathi lisusiwe" "Ifayela lokungena lesikhathi alisusiwe" + "Umlando wesikhathi ukhiyiwe" + "Iphutha: Umlando wesikhathi awukhiyiwe" + "Ukungena kwesikhathi kunikwe amandla" "Izilimi zokufakwayo" "Thinta futhi ukuze ulondoloze" "Isichazamazwi siyatholakala" diff --git a/java/res/values/strings-appname.xml b/java/res/values/strings-appname.xml new file mode 100644 index 000000000..19aaa2513 --- /dev/null +++ b/java/res/values/strings-appname.xml @@ -0,0 +1,33 @@ + + + + + + Android keyboard + + + Android spell checker + + + Android keyboard settings + + + Spell checking settings + diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 6c66a4844..7272cfe97 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -18,25 +18,16 @@ */ --> - - Android keyboard Android keyboard (AOSP) - - Android keyboard settings Input options Research Log Commands - - Android spell checker Android spell checker (AOSP) - - Spell checking settings - Look up contact names @@ -233,12 +224,22 @@ Do not log this session + + Enable session logging + + Log whole session history Deleting session log Session log deleted Session log NOT deleted + + Session history logged + + Error: Session history NOT logged + + Session logging enabled Input languages diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 32ef408b4..1ae0020a4 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -610,6 +610,15 @@ public class PointerTracker { onUpEventInternal(); onDownEventInternal(x, y, eventTime); } else { + // HACK: If there are currently multiple touches, register the key even if + // the finger slides off the key. This defends against noise from some + // touch panels when there are close multiple touches. + // Caveat: When in chording input mode with a modifier key, we don't use + // this hack. + if (me != null && me.getPointerCount() > 1 + && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) { + onUpEventInternal(); + } mKeyAlreadyProcessed = true; setReleasedKeyGraphics(oldKey); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 5db65c660..d3bb85d4b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -28,6 +28,7 @@ public class PointerTrackerQueue { private static final String TAG = PointerTrackerQueue.class.getSimpleName(); private static final boolean DEBUG = false; + // TODO: Use ring buffer instead of {@link LinkedList}. private final LinkedList mQueue = new LinkedList(); public synchronized void add(PointerTracker tracker) { @@ -81,6 +82,20 @@ public class PointerTrackerQueue { } } + public synchronized boolean hasModifierKeyOlderThan(PointerTracker tracker) { + final Iterator it = mQueue.iterator(); + while (it.hasNext()) { + final PointerTracker t = it.next(); + if (t == tracker) { + break; + } + if (t.isModifier()) { + return true; + } + } + return false; + } + public synchronized boolean isAnyInSlidingKeyInput() { for (final PointerTracker tracker : mQueue) { if (tracker.isInSlidingKeyInput()) { diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java index c78974dac..a66337404 100644 --- a/java/src/com/android/inputmethod/latin/AutoCorrection.java +++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java @@ -92,7 +92,8 @@ public class AutoCorrection { public static boolean suggestionExceedsAutoCorrectionThreshold(SuggestedWordInfo suggestion, CharSequence consideredWord, float autoCorrectionThreshold) { if (null != suggestion) { - //final int autoCorrectionSuggestionScore = sortedScores[0]; + // Shortlist a whitelisted word + if (suggestion.mKind == SuggestedWordInfo.KIND_WHITELIST) return true; final int autoCorrectionSuggestionScore = suggestion.mScore; // TODO: when the normalized score of the first suggestion is nearly equals to // the normalized score of the second suggestion, behave less aggressive. diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index ae415d0ab..534cffb2d 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -45,17 +45,17 @@ public class BinaryDictionary extends Dictionary { public static final int MAX_SPACES = 16; private static final String TAG = "BinaryDictionary"; - private static final int MAX_BIGRAMS = 60; + private static final int MAX_PREDICTIONS = 60; + private static final int MAX_RESULTS = Math.max(MAX_PREDICTIONS, MAX_WORDS); private static final int TYPED_LETTER_MULTIPLIER = 2; private long mNativeDict; private final int[] mInputCodes = new int[MAX_WORD_LENGTH]; - private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; - private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS]; + private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_RESULTS]; private final int[] mSpaceIndices = new int[MAX_SPACES]; - private final int[] mScores = new int[MAX_WORDS]; - private final int[] mBigramScores = new int[MAX_BIGRAMS]; + private final int[] mOutputScores = new int[MAX_RESULTS]; + private final int[] mOutputTypes = new int[MAX_RESULTS]; private final boolean mUseFullEditDistance; @@ -83,7 +83,8 @@ public class BinaryDictionary extends Dictionary { } private native long openNative(String sourceDir, long dictOffset, long dictSize, - int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords); + int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords, + int maxPredictions); private native void closeNative(long dict); private native int getFrequencyNative(long dict, int[] word, int wordLength); private native boolean isValidBigramNative(long dict, int[] word1, int[] word2); @@ -91,78 +92,59 @@ public class BinaryDictionary extends Dictionary { int[] yCoordinates, int[] times, int[] pointerIds, int[] inputCodes, int codesSize, int commitPoint, boolean isGesture, int[] prevWordCodePointArray, boolean useFullEditDistance, char[] outputChars, - int[] scores, int[] outputIndices); - private native int getBigramsNative(long dict, int[] prevWord, int prevWordLength, - int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores, - int maxWordLength, int maxBigrams); + int[] outputScores, int[] outputIndices, int[] outputTypes); private static native float calcNormalizedScoreNative( char[] before, int beforeLength, char[] after, int afterLength, int score); private static native int editDistanceNative( char[] before, int beforeLength, char[] after, int afterLength); private final void loadDictionary(String path, long startOffset, long length) { - mNativeDict = openNative(path, startOffset, length, - TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS); + mNativeDict = openNative(path, startOffset, length, TYPED_LETTER_MULTIPLIER, + FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS, MAX_PREDICTIONS); } @Override - public ArrayList getBigrams(final WordComposer codes, - final CharSequence previousWord) { - if (mNativeDict == 0) return null; + public ArrayList getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { + if (!isValidDictionary()) return null; + Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE); + Arrays.fill(mOutputChars, (char) 0); + Arrays.fill(mOutputScores, 0); + // TODO: toLowerCase in the native code + final int[] prevWordCodePointArray = (null == prevWord) + ? null : StringUtils.toCodePointArray(prevWord.toString()); + final int composerSize = composer.size(); - int[] codePoints = StringUtils.toCodePointArray(previousWord.toString()); - Arrays.fill(mOutputChars_bigrams, (char) 0); - Arrays.fill(mBigramScores, 0); - - int codesSize = codes.size(); - Arrays.fill(mInputCodes, -1); - if (codesSize > 0) { - mInputCodes[0] = codes.getCodeAt(0); + final boolean isGesture = composer.isBatchMode(); + if (composerSize <= 1 || !isGesture) { + if (composerSize > MAX_WORD_LENGTH - 1) return null; + for (int i = 0; i < composerSize; i++) { + mInputCodes[i] = composer.getCodeAt(i); + } } - int count = getBigramsNative(mNativeDict, codePoints, codePoints.length, mInputCodes, - codesSize, mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS); - if (count > MAX_BIGRAMS) { - count = MAX_BIGRAMS; - } + final InputPointers ips = composer.getInputPointers(); + final int codesSize = isGesture ? ips.getPointerSize() : composerSize; + // proximityInfo and/or prevWordForBigrams may not be null. + final int tmpCount = getSuggestionsNative(mNativeDict, + proximityInfo.getNativeProximityInfo(), ips.getXCoordinates(), + ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), + mInputCodes, codesSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray, + mUseFullEditDistance, mOutputChars, mOutputScores, mSpaceIndices, mOutputTypes); + final int count = Math.min(tmpCount, MAX_PREDICTIONS); final ArrayList suggestions = new ArrayList(); for (int j = 0; j < count; ++j) { - if (codesSize > 0 && mBigramScores[j] < 1) break; + if (composerSize > 0 && mOutputScores[j] < 1) break; final int start = j * MAX_WORD_LENGTH; int len = 0; - while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) { + while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) { ++len; } if (len > 0) { - suggestions.add(new SuggestedWordInfo( - new String(mOutputChars_bigrams, start, len), - mBigramScores[j], SuggestedWordInfo.KIND_CORRECTION, mDictType)); - } - } - return suggestions; - } - - // proximityInfo and/or prevWordForBigrams may not be null. - @Override - public ArrayList getWords(final WordComposer codes, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { - final int count = getSuggestions(codes, prevWordForBigrams, proximityInfo, mOutputChars, - mScores, mSpaceIndices); - - final ArrayList suggestions = new ArrayList(); - for (int j = 0; j < count; ++j) { - if (mScores[j] < 1) break; - final int start = j * MAX_WORD_LENGTH; - int len = 0; - while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) { - ++len; - } - if (len > 0) { - // TODO: actually get the kind from native code suggestions.add(new SuggestedWordInfo( new String(mOutputChars, start, len), - mScores[j], SuggestedWordInfo.KIND_CORRECTION, mDictType)); + mOutputScores[j], SuggestedWordInfo.KIND_CORRECTION, mDictType)); } } return suggestions; @@ -172,41 +154,6 @@ public class BinaryDictionary extends Dictionary { return mNativeDict != 0; } - // proximityInfo may not be null. - /* package for test */ int getSuggestions(final WordComposer codes, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo, - char[] outputChars, int[] scores, int[] spaceIndices) { - if (!isValidDictionary()) return -1; - - final int codesSize = codes.size(); - // Won't deal with really long words. - if (codesSize > MAX_WORD_LENGTH - 1) return -1; - - Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE); - for (int i = 0; i < codesSize; i++) { - mInputCodes[i] = codes.getCodeAt(i); - } - Arrays.fill(outputChars, (char) 0); - Arrays.fill(scores, 0); - - // TODO: toLowerCase in the native code - final int[] prevWordCodePointArray = (null == prevWordForBigrams) - ? null : StringUtils.toCodePointArray(prevWordForBigrams.toString()); - - int[] emptyArray = new int[codesSize]; - Arrays.fill(emptyArray, 0); - - //final int commitPoint = codes.getCommitPoint(); - //codes.clearCommitPoint(); - - final InputPointers ips = codes.getInputPointers(); - - return getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), - ips.getXCoordinates(), ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), - mInputCodes, codesSize, 0 /* unused */, false, prevWordCodePointArray, - mUseFullEditDistance, outputChars, scores, spaceIndices); - } - public static float calcNormalizedScore(String before, String after, int score) { return calcNormalizedScoreNative(before.toCharArray(), before.length(), after.toCharArray(), after.length(), score); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 37eced5d6..236c198ad 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -149,7 +149,8 @@ public class BinaryDictionaryFileDumper { final int MODE_MAX = NONE; final Uri.Builder wordListUriBuilder = getProviderUriBuilder(id); - final String outputFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context); + final String finalFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context); + final String tempFileName = finalFileName + ".tmp"; for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) { InputStream originalSourceStream = null; @@ -165,7 +166,10 @@ public class BinaryDictionaryFileDumper { if (null == afd) return null; originalSourceStream = afd.createInputStream(); // Open output. - outputFile = new File(outputFileName); + outputFile = new File(tempFileName); + // Just to be sure, delete the file. This may fail silently, and return false: this + // is the right thing to do, as we just want to continue anyway. + outputFile.delete(); outputStream = new FileOutputStream(outputFile); // Get the appropriate decryption method for this try switch (mode) { @@ -194,14 +198,20 @@ public class BinaryDictionaryFileDumper { break; } checkMagicAndCopyFileTo(new BufferedInputStream(inputStream), outputStream); + outputStream.flush(); + outputStream.close(); + final File finalFile = new File(finalFileName); + if (!outputFile.renameTo(finalFile)) { + throw new IOException("Can't move the file to its final name"); + } wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, QUERY_PARAMETER_SUCCESS); if (0 >= resolver.delete(wordListUriBuilder.build(), null, null)) { Log.e(TAG, "Could not have the dictionary pack delete a word list"); } - BinaryDictionaryGetter.removeFilesWithIdExcept(context, id, outputFile); + BinaryDictionaryGetter.removeFilesWithIdExcept(context, id, finalFile); // Success! Close files (through the finally{} clause) and return. - return AssetFileAddress.makeFromFileName(outputFileName); + return AssetFileAddress.makeFromFileName(finalFileName); } catch (Exception e) { if (DEBUG) { Log.i(TAG, "Can't open word list in mode " + mode + " : " + e); diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 0835450c1..fd40aa6da 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import android.text.TextUtils; + import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; @@ -50,24 +52,17 @@ public abstract class Dictionary { } /** - * Searches for words in the dictionary that match the characters in the composer. Matched - * words are returned as an ArrayList. + * Searches for suggestions for a given context. For the moment the context is only the + * previous word. * @param composer the key sequence to match with coordinate info, as a WordComposer - * @param prevWordForBigrams the previous word, or null if none + * @param prevWord the previous word, or null if none * @param proximityInfo the object for key proximity. May be ignored by some implementations. - * @return the list of suggestions + * @return the list of suggestions (possibly null if none) */ - abstract public ArrayList getWords(final WordComposer composer, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo); - - /** - * Searches for pairs in the bigram dictionary that matches the previous word. - * @param composer the key sequence to match - * @param previousWord the word before - * @return the list of suggestions - */ - public abstract ArrayList getBigrams(final WordComposer composer, - final CharSequence previousWord); + // TODO: pass more context than just the previous word, to enable better suggestions (n-gram + // and more) + abstract public ArrayList getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo); /** * Checks if the given word occurs in the dictionary diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index dcc53c59f..88ac07d7a 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -55,38 +55,19 @@ public class DictionaryCollection extends Dictionary { } @Override - public ArrayList getWords(final WordComposer composer, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { + public ArrayList getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { final CopyOnWriteArrayList dictionaries = mDictionaries; if (dictionaries.isEmpty()) return null; // To avoid creating unnecessary objects, we get the list out of the first // dictionary and add the rest to it if not null, hence the get(0) - ArrayList suggestions = dictionaries.get(0).getWords(composer, - prevWordForBigrams, proximityInfo); + ArrayList suggestions = dictionaries.get(0).getSuggestions(composer, + prevWord, proximityInfo); if (null == suggestions) suggestions = new ArrayList(); final int length = dictionaries.size(); for (int i = 0; i < length; ++ i) { - final ArrayList sugg = dictionaries.get(i).getWords(composer, - prevWordForBigrams, proximityInfo); - if (null != sugg) suggestions.addAll(sugg); - } - return suggestions; - } - - @Override - public ArrayList getBigrams(final WordComposer composer, - final CharSequence previousWord) { - final CopyOnWriteArrayList dictionaries = mDictionaries; - if (dictionaries.isEmpty()) return null; - // To avoid creating unnecessary objects, we get the list out of the first - // dictionary and add the rest to it if not null, hence the get(0) - ArrayList suggestions = dictionaries.get(0).getBigrams(composer, - previousWord); - if (null == suggestions) suggestions = new ArrayList(); - final int length = dictionaries.size(); - for (int i = 0; i < length; ++ i) { - final ArrayList sugg = - dictionaries.get(i).getBigrams(composer, previousWord); + final ArrayList sugg = dictionaries.get(i).getSuggestions(composer, + prevWord, proximityInfo); if (null != sugg) suggestions.addAll(sugg); } return suggestions; diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 1cda9f257..016530abb 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -192,41 +192,13 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } @Override - public ArrayList getWords(final WordComposer codes, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { + public ArrayList getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { asyncReloadDictionaryIfRequired(); - return getWordsInner(codes, prevWordForBigrams, proximityInfo); - } - - protected final ArrayList getWordsInner(final WordComposer codes, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { - // Ensure that there are no concurrent calls to getWords. If there are, do nothing and - // return. if (mLocalDictionaryController.tryLock()) { try { if (mBinaryDictionary != null) { - return mBinaryDictionary.getWords(codes, prevWordForBigrams, proximityInfo); - } - } finally { - mLocalDictionaryController.unlock(); - } - } - return null; - } - - @Override - public ArrayList getBigrams(final WordComposer codes, - final CharSequence previousWord) { - asyncReloadDictionaryIfRequired(); - return getBigramsInner(codes, previousWord); - } - - protected ArrayList getBigramsInner(final WordComposer codes, - final CharSequence previousWord) { - if (mLocalDictionaryController.tryLock()) { - try { - if (mBinaryDictionary != null) { - return mBinaryDictionary.getBigrams(codes, previousWord); + return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo); } } finally { mLocalDictionaryController.unlock(); diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 76213c0da..5d7995dc2 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin; import android.content.Context; +import android.text.TextUtils; import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; @@ -247,23 +248,36 @@ public class ExpandableDictionary extends Dictionary { } @Override - public ArrayList getWords(final WordComposer codes, - final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { + public ArrayList getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { + if (reloadDictionaryIfRequired()) return null; + if (composer.size() <= 1) { + if (composer.size() >= BinaryDictionary.MAX_WORD_LENGTH) { + return null; + } + final ArrayList suggestions = + getWordsInner(composer, prevWord, proximityInfo); + return suggestions; + } else { + if (TextUtils.isEmpty(prevWord)) return null; + final ArrayList suggestions = new ArrayList(); + runBigramReverseLookUp(prevWord, suggestions); + return suggestions; + } + } + + // This reloads the dictionary if required, and returns whether it's currently updating its + // contents or not. + // @VisibleForTesting + boolean reloadDictionaryIfRequired() { synchronized (mUpdatingLock) { // If we need to update, start off a background task if (mRequiresReload) startDictionaryLoadingTaskLocked(); - // Currently updating contacts, don't return any results. - if (mUpdatingDictionary) return null; + return mUpdatingDictionary; } - if (codes.size() >= BinaryDictionary.MAX_WORD_LENGTH) { - return null; - } - final ArrayList suggestions = - getWordsInner(codes, prevWordForBigrams, proximityInfo); - return suggestions; } - protected final ArrayList getWordsInner(final WordComposer codes, + protected ArrayList getWordsInner(final WordComposer codes, final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { final ArrayList suggestions = new ArrayList(); mInputLength = codes.size(); @@ -589,16 +603,6 @@ public class ExpandableDictionary extends Dictionary { return searchWord(childNode.mChildren, word, depth + 1, childNode); } - // @VisibleForTesting - boolean reloadDictionaryIfRequired() { - synchronized (mUpdatingLock) { - // If we need to update, start off a background task - if (mRequiresReload) startDictionaryLoadingTaskLocked(); - // Currently updating contacts, don't return any results. - return mUpdatingDictionary; - } - } - private void runBigramReverseLookUp(final CharSequence previousWord, final ArrayList suggestions) { // Search for the lowercase version of the word only, because that's where bigrams @@ -610,17 +614,6 @@ public class ExpandableDictionary extends Dictionary { } } - @Override - public ArrayList getBigrams(final WordComposer codes, - final CharSequence previousWord) { - if (!reloadDictionaryIfRequired()) { - final ArrayList suggestions = new ArrayList(); - runBigramReverseLookUp(previousWord, suggestions); - return suggestions; - } - return null; - } - /** * Used for testing purposes and in the spell checker * This function will wait for loading from database to be done diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index 9d77d4e96..5ad53480f 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -18,7 +18,7 @@ package com.android.inputmethod.latin; import java.util.Arrays; -// TODO: Add unit test +// TODO: This class is not thread-safe. public class InputPointers { private final ScalableIntArray mXCoordinates = new ScalableIntArray(); private final ScalableIntArray mYCoordinates = new ScalableIntArray(); @@ -55,21 +55,15 @@ public class InputPointers { /** * Append the pointers in the specified {@link InputPointers} to the end of this. - * @param src the source {@link InputPointers} to append the pointers. + * @param src the source {@link InputPointers} to read the data from. * @param startPos the starting index of the pointers in {@code src}. * @param length the number of pointers to be appended. */ public void append(InputPointers src, int startPos, int length) { - final int currentLength = getPointerSize(); - final int newLength = currentLength + length; - mXCoordinates.ensureCapacity(newLength); - mYCoordinates.ensureCapacity(newLength); - mPointerIds.ensureCapacity(newLength); - mTimes.ensureCapacity(newLength); - System.arraycopy(src.getXCoordinates(), startPos, getXCoordinates(), currentLength, length); - System.arraycopy(src.getYCoordinates(), startPos, getYCoordinates(), currentLength, length); - System.arraycopy(src.getPointerIds(), startPos, getPointerIds(), currentLength, length); - System.arraycopy(src.getTimes(), startPos, getTimes(), currentLength, length); + mXCoordinates.append(src.mXCoordinates, startPos, length); + mYCoordinates.append(src.mYCoordinates, startPos, length); + mPointerIds.append(src.mPointerIds, startPos, length); + mTimes.append(src.mTimes, startPos, length); } public void reset() { @@ -118,24 +112,23 @@ public class InputPointers { } public void add(int val) { - ensureCapacity(mLength); + final int nextLength = mLength + 1; + ensureCapacity(nextLength); mArray[mLength] = val; - ++mLength; + mLength = nextLength; } - public void ensureCapacity(int minimumCapacity) { + private void ensureCapacity(int minimumCapacity) { if (mArray.length < minimumCapacity) { final int nextCapacity = mArray.length * 2; - grow(minimumCapacity > nextCapacity ? minimumCapacity : nextCapacity); + // The following is the same as newLength = Math.max(minimumCapacity, nextCapacity); + final int newLength = minimumCapacity > nextCapacity + ? minimumCapacity + : nextCapacity; + mArray = Arrays.copyOf(mArray, newLength); } } - private void grow(int newCapacity) { - final int[] newArray = new int[newCapacity]; - System.arraycopy(mArray, 0, newArray, 0, mLength); - mArray = newArray; - } - public int getLength() { return mLength; } @@ -149,14 +142,23 @@ public class InputPointers { return mArray; } - public void copy(ScalableIntArray ip) { - mArray = Arrays.copyOf(ip.mArray, ip.mArray.length); - mLength = ip.mLength; - } - public void set(ScalableIntArray ip) { mArray = ip.mArray; mLength = ip.mLength; } + + public void copy(ScalableIntArray ip) { + ensureCapacity(ip.mLength); + System.arraycopy(ip.mArray, 0, mArray, 0, ip.mLength); + mLength = ip.mLength; + } + + public void append(ScalableIntArray src, int startPos, int length) { + final int currentLength = mLength; + final int newLength = currentLength + length; + ensureCapacity(newLength); + System.arraycopy(src.mArray, startPos, mArray, currentLength, length); + mLength = newLength; + } } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 4be2a1799..f27d32150 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -353,7 +353,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mPrefs = prefs; LatinImeLogger.init(this, prefs); if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.getInstance().init(this, prefs); + ResearchLogger.getInstance().init(this, prefs, mKeyboardSwitcher); } InputMethodManagerCompatWrapper.init(this); SubtypeSwitcher.init(this); @@ -689,6 +689,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } + // Callback for the TargetApplicationGetter @Override public void onTargetApplicationKnown(final ApplicationInfo info) { mTargetApplicationInfo = info; @@ -997,7 +998,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; } - public void commitTyped(final int separatorCode) { + private void commitTyped(final int separatorCode) { if (!mWordComposer.isComposingWord()) return; final CharSequence typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { @@ -1013,6 +1014,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen updateSuggestionsOrPredictions(); } + // Called from the KeyboardSwitcher which needs to know auto caps state to display + // the right layout. public int getCurrentAutoCapsState() { if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; @@ -1088,6 +1091,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET; } + // Callback for the SuggestionsView, to call when the "add to dictionary" hint is pressed. @Override public boolean addWordToUserDictionary(String word) { mUserDictionary.addWordToUserDictionary(word, 128); @@ -1285,6 +1289,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mConnection.endBatchEdit(); } + // Called from PointerTracker through the KeyboardActionListener interface @Override public void onTextInput(CharSequence text) { mConnection.beginBatchEdit(); @@ -1348,6 +1353,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } + // Called from PointerTracker through the KeyboardActionListener interface @Override public void onCancelInput() { // User released a finger outside any key @@ -1505,7 +1511,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // it entirely and resume suggestions on the previous word, we'd like to still // have touch coordinates for it. resetComposingState(false /* alsoResetLastComposedWord */); - clearSuggestions(); } if (isComposingWord) { final int keyX, keyY; @@ -1621,12 +1626,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen inputView.closing(); } - public boolean isShowingPunctuationList() { + // TODO: make this private + // Outside LatinIME, only used by the test suite. + /* package for tests */ boolean isShowingPunctuationList() { if (mSuggestionsView == null) return false; return mCurrentSettings.mSuggestPuncList == mSuggestionsView.getSuggestions(); } - public boolean isSuggestionsStripVisible() { + private boolean isSuggestionsStripVisible() { if (mSuggestionsView == null) return false; if (mSuggestionsView.isShowingAddToDictionaryHint()) @@ -1638,7 +1645,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation); } - public void clearSuggestions() { + private void clearSuggestions() { setSuggestions(SuggestedWords.EMPTY, false); setAutoCorrectionIndicator(false); } @@ -1661,7 +1668,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - public void updateSuggestionsOrPredictions() { + // TODO: rename this method to updateSuggestionStrip or simply updateSuggestions + private void updateSuggestionsOrPredictions() { mHandler.cancelUpdateSuggestionStrip(); // Check if we have a suggestion engine attached. @@ -1675,42 +1683,36 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } final String typedWord = mWordComposer.getTypedWord(); - final SuggestedWords suggestions; if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) { setPunctuationSuggestions(); return; } - if (!mWordComposer.isComposingWord()) { - suggestions = updateBigramPredictions(); - } else { - suggestions = updateSuggestions(typedWord); - } + // Get the word on which we should search the bigrams. If we are composing a word, it's + // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we + // should just skip whitespace if any, so 1. + // TODO: this is slow (2-way IPC) - we should probably cache this instead. + final CharSequence prevWord = + mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, + mWordComposer.isComposingWord() ? 2 : 1); + SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer, + prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), + mCurrentSettings.mCorrectionEnabled); + suggestedWords = maybeRetrieveOlderSuggestions(typedWord, suggestedWords); - if (null != suggestions && suggestions.size() > 0) { - showSuggestions(suggestions, typedWord); - } else { - clearSuggestions(); - } + showSuggestions(suggestedWords, typedWord); } - private SuggestedWords updateSuggestions(final CharSequence typedWord) { - // TODO: May need a better way of retrieving previous word - final CharSequence prevWord = - mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2); - // getSuggestedWords handles gracefully a null value of prevWord - final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer, - prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), - // !mWordComposer.isComposingWord() is known to be false - mCurrentSettings.mCorrectionEnabled, !mWordComposer.isComposingWord()); - - // Basically, we update the suggestion strip only when suggestion count > 1. However, - // there is an exception: We update the suggestion strip whenever typed word's length - // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually, - // in most cases, suggestion count is 1 when typed word's length is 1, but we do always - // need to clear the previous state when the user starts typing a word (i.e. typed word's - // length == 1). - if (suggestedWords.size() > 1 || typedWord.length() == 1 + private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord, + final SuggestedWords suggestedWords) { + // TODO: consolidate this into getSuggestedWords + // We update the suggestion strip only when we have some suggestions to show, i.e. when + // the suggestion count is > 1; else, we leave the old suggestions, with the typed word + // replaced with the new one. However, when the word is a dictionary word, or when the + // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the + // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to + // revert to suggestions - although it is unclear how we can come here if it's displayed. + if (suggestedWords.size() > 1 || typedWord.length() <= 1 || !suggestedWords.mTypedWordValid || mSuggestionsView.isShowingAddToDictionaryHint()) { return suggestedWords; @@ -1733,7 +1735,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) { - // This method is only ever called by updateSuggestions or updateBigramPredictions. + if (null == suggestedWords || suggestedWords.size() <= 0) { + clearSuggestions(); + return; + } final CharSequence autoCorrection; if (suggestedWords.size() > 0) { if (suggestedWords.mWillAutoCorrect) { @@ -1781,6 +1786,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } + // Called from SuggestionsView through the SuggestionsView.Listener interface @Override public void pickSuggestionManually(final int index, final CharSequence suggestion, final int x, final int y) { @@ -1891,16 +1897,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen separatorCode, prevWord); } - private SuggestedWords updateBigramPredictions() { - final CharSequence prevWord = - mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 1); - return mSuggest.getSuggestedWords(mWordComposer, - prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), - // !mWordComposer.isComposingWord() is known to be true - mCurrentSettings.mCorrectionEnabled, !mWordComposer.isComposingWord()); - } - - public void setPunctuationSuggestions() { + private void setPunctuationSuggestions() { if (mCurrentSettings.mBigramPredictionEnabled) { clearSuggestions(); } else { @@ -2008,16 +2005,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.postUpdateSuggestionStrip(); } + // Used by the RingCharBuffer public boolean isWordSeparator(int code) { return mCurrentSettings.isWordSeparator(code); } - public boolean preferCapitalization() { - return mWordComposer.isFirstCharCapitalized(); - } - // Notify that language or mode have been changed and toggleLanguage will update KeyboardID - // according to new language or mode. + // according to new language or mode. Called from SubtypeSwitcher. public void onRefreshKeyboard() { // When the device locale is changed in SetupWizard etc., this method may get called via // onConfigurationChanged before SoftInputWindow is shown. @@ -2034,16 +2028,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to - // {@link KeyboardSwitcher}. + // {@link KeyboardSwitcher}. Called from KeyboardSwitcher public void hapticAndAudioFeedback(final int primaryCode) { mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView()); } + // Callback called by PointerTracker through the KeyboardActionListener. This is called when a + // key is depressed; release matching call is onReleaseKey below. @Override public void onPressKey(int primaryCode) { mKeyboardSwitcher.onPressKey(primaryCode); } + // Callback by PointerTracker through the KeyboardActionListener. This is called when a key + // is released; press matching call is onPressKey above. @Override public void onReleaseKey(int primaryCode, boolean withSliding) { mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); @@ -2088,6 +2086,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen launchSettingsClass(SettingsActivity.class); } + // Called from debug code only public void launchDebugSettings() { launchSettingsClass(DebugSettingsActivity.class); } diff --git a/java/src/com/android/inputmethod/latin/ResearchLog.java b/java/src/com/android/inputmethod/latin/ResearchLog.java new file mode 100644 index 000000000..1de5cb36a --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ResearchLog.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import android.content.SharedPreferences; +import android.os.SystemClock; +import android.util.JsonWriter; +import android.util.Log; +import android.view.inputmethod.CompletionInfo; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.ResearchLogger.LogUnit; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.define.ProductionFlag; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Logs the use of the LatinIME keyboard. + * + * This class logs operations on the IME keyboard, including what the user has typed. + * Data is stored locally in a file in app-specific storage. + * + * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. + */ +public class ResearchLog { + private static final String TAG = ResearchLog.class.getSimpleName(); + private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( + new OutputStreamWriter(new NullOutputStream())); + + final ScheduledExecutorService mExecutor; + /* package */ final File mFile; + private JsonWriter mJsonWriter = NULL_JSON_WRITER; // should never be null + + private int mLoggingState; + private static final int LOGGING_STATE_UNSTARTED = 0; + private static final int LOGGING_STATE_RUNNING = 1; + private static final int LOGGING_STATE_STOPPING = 2; + private static final int LOGGING_STATE_STOPPED = 3; + private static final long FLUSH_DELAY_IN_MS = 1000 * 5; + + private static class NullOutputStream extends OutputStream { + /** {@inheritDoc} */ + @Override + public void write(byte[] buffer, int offset, int count) { + // nop + } + + /** {@inheritDoc} */ + @Override + public void write(byte[] buffer) { + // nop + } + + @Override + public void write(int oneByte) { + } + } + + public ResearchLog(File outputFile) { + mExecutor = Executors.newSingleThreadScheduledExecutor(); + if (outputFile == null) { + throw new IllegalArgumentException(); + } + mFile = outputFile; + mLoggingState = LOGGING_STATE_UNSTARTED; + } + + public synchronized void start() throws IOException { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); + mJsonWriter.setLenient(true); + mJsonWriter.beginArray(); + mLoggingState = LOGGING_STATE_RUNNING; + break; + case LOGGING_STATE_RUNNING: + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + break; + } + } + + public synchronized void stop() { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + mLoggingState = LOGGING_STATE_STOPPED; + break; + case LOGGING_STATE_RUNNING: + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + try { + mJsonWriter.endArray(); + mJsonWriter.flush(); + mJsonWriter.close(); + } finally { + // the contentprovider only exports data if the writable + // bit is cleared. + boolean success = mFile.setWritable(false, false); + mLoggingState = LOGGING_STATE_STOPPED; + } + return null; + } + }); + mExecutor.shutdown(); + mLoggingState = LOGGING_STATE_STOPPING; + break; + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + } + } + + public boolean isAlive() { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + case LOGGING_STATE_RUNNING: + return true; + } + return false; + } + + public void waitUntilStopped(int timeoutInMs) throws InterruptedException { + mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); + } + + private boolean isAbortSuccessful; + public boolean isAbortSuccessful() { + return isAbortSuccessful; + } + + public synchronized void abort() { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + mLoggingState = LOGGING_STATE_STOPPED; + isAbortSuccessful = true; + break; + case LOGGING_STATE_RUNNING: + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + try { + mJsonWriter.endArray(); + mJsonWriter.close(); + } finally { + isAbortSuccessful = mFile.delete(); + } + return null; + } + }); + mExecutor.shutdown(); + mLoggingState = LOGGING_STATE_STOPPING; + break; + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + } + } + + /* package */ synchronized void flush() { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + break; + case LOGGING_STATE_RUNNING: + removeAnyScheduledFlush(); + mExecutor.submit(mFlushCallable); + break; + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + } + } + + private Callable mFlushCallable = new Callable() { + @Override + public Object call() throws Exception { + mJsonWriter.flush(); + return null; + } + }; + + private ScheduledFuture mFlushFuture; + + private void removeAnyScheduledFlush() { + if (mFlushFuture != null) { + mFlushFuture.cancel(false); + mFlushFuture = null; + } + } + + private void scheduleFlush() { + removeAnyScheduledFlush(); + mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); + } + + public synchronized void publishPublicEvents(final LogUnit logUnit) { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + break; + case LOGGING_STATE_RUNNING: + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + logUnit.publishPublicEventsTo(ResearchLog.this); + scheduleFlush(); + return null; + } + }); + break; + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + } + } + + public synchronized void publishAllEvents(final LogUnit logUnit) { + switch (mLoggingState) { + case LOGGING_STATE_UNSTARTED: + break; + case LOGGING_STATE_RUNNING: + mExecutor.submit(new Callable() { + @Override + public Object call() throws Exception { + logUnit.publishAllEventsTo(ResearchLog.this); + scheduleFlush(); + return null; + } + }); + break; + case LOGGING_STATE_STOPPING: + case LOGGING_STATE_STOPPED: + } + } + + private static final String CURRENT_TIME_KEY = "_ct"; + private static final String UPTIME_KEY = "_ut"; + private static final String EVENT_TYPE_KEY = "_ty"; + void outputEvent(final String[] keys, final Object[] values) { + // not thread safe. + try { + mJsonWriter.beginObject(); + mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); + mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis()); + mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]); + final int length = values.length; + for (int i = 0; i < length; i++) { + mJsonWriter.name(keys[i + 1]); + Object value = values[i]; + if (value instanceof String) { + mJsonWriter.value((String) value); + } else if (value instanceof Number) { + mJsonWriter.value((Number) value); + } else if (value instanceof Boolean) { + mJsonWriter.value((Boolean) value); + } else if (value instanceof CompletionInfo[]) { + CompletionInfo[] ci = (CompletionInfo[]) value; + mJsonWriter.beginArray(); + for (int j = 0; j < ci.length; j++) { + mJsonWriter.value(ci[j].toString()); + } + mJsonWriter.endArray(); + } else if (value instanceof SharedPreferences) { + SharedPreferences prefs = (SharedPreferences) value; + mJsonWriter.beginObject(); + for (Map.Entry entry : prefs.getAll().entrySet()) { + mJsonWriter.name(entry.getKey()); + final Object innerValue = entry.getValue(); + if (innerValue == null) { + mJsonWriter.nullValue(); + } else if (innerValue instanceof Boolean) { + mJsonWriter.value((Boolean) innerValue); + } else if (innerValue instanceof Number) { + mJsonWriter.value((Number) innerValue); + } else { + mJsonWriter.value(innerValue.toString()); + } + } + mJsonWriter.endObject(); + } else if (value instanceof Key[]) { + Key[] keyboardKeys = (Key[]) value; + mJsonWriter.beginArray(); + for (Key keyboardKey : keyboardKeys) { + mJsonWriter.beginObject(); + mJsonWriter.name("code").value(keyboardKey.mCode); + mJsonWriter.name("altCode").value(keyboardKey.mAltCode); + mJsonWriter.name("x").value(keyboardKey.mX); + mJsonWriter.name("y").value(keyboardKey.mY); + mJsonWriter.name("w").value(keyboardKey.mWidth); + mJsonWriter.name("h").value(keyboardKey.mHeight); + mJsonWriter.endObject(); + } + mJsonWriter.endArray(); + } else if (value instanceof SuggestedWords) { + SuggestedWords words = (SuggestedWords) value; + mJsonWriter.beginObject(); + mJsonWriter.name("typedWordValid").value(words.mTypedWordValid); + mJsonWriter.name("willAutoCorrect") + .value(words.mWillAutoCorrect); + mJsonWriter.name("isPunctuationSuggestions") + .value(words.mIsPunctuationSuggestions); + mJsonWriter.name("isObsoleteSuggestions") + .value(words.mIsObsoleteSuggestions); + mJsonWriter.name("isPrediction") + .value(words.mIsPrediction); + mJsonWriter.name("words"); + mJsonWriter.beginArray(); + final int size = words.size(); + for (int j = 0; j < size; j++) { + SuggestedWordInfo wordInfo = words.getWordInfo(j); + mJsonWriter.value(wordInfo.toString()); + } + mJsonWriter.endArray(); + mJsonWriter.endObject(); + } else if (value == null) { + mJsonWriter.nullValue(); + } else { + Log.w(TAG, "Unrecognized type to be logged: " + + (value == null ? "" : value.getClass().getName())); + mJsonWriter.nullValue(); + } + } + mJsonWriter.endObject(); + } catch (IOException e) { + e.printStackTrace(); + Log.w(TAG, "Error in JsonWriter; disabling logging"); + try { + mJsonWriter.close(); + } catch (IllegalStateException e1) { + // assume that this is just the json not being terminated properly. + // ignore + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + mJsonWriter = NULL_JSON_WRITER; + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java index 2de0194fd..9055d5d32 100644 --- a/java/src/com/android/inputmethod/latin/ResearchLogger.java +++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java @@ -24,12 +24,7 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.inputmethodservice.InputMethodService; import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Process; -import android.os.SystemClock; import android.text.TextUtils; -import android.util.JsonWriter; import android.util.Log; import android.view.MotionEvent; import android.view.inputmethod.CompletionInfo; @@ -40,22 +35,17 @@ import android.widget.Toast; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; +import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.latin.RichInputConnection.Range; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileWriter; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.UUID; /** @@ -68,36 +58,35 @@ import java.util.UUID; */ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = ResearchLogger.class.getSimpleName(); - private static final boolean DEBUG = false; private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info /* package */ static boolean sIsLogging = false; private static final int OUTPUT_FORMAT_VERSION = 1; private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; private static final String FILENAME_PREFIX = "researchLog"; private static final String FILENAME_SUFFIX = ".txt"; - private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( - new OutputStreamWriter(new NullOutputStream())); private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = - new SimpleDateFormat("yyyyMMddHHmmss", Locale.US); + new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; + private static final int ABORT_TIMEOUT_IN_MS = 10 * 1000; private static final ResearchLogger sInstance = new ResearchLogger(); - private HandlerThread mHandlerThread; - /* package */ Handler mLoggingHandler; // to write to a different filename, e.g., for testing, set mFile before calling start() - private File mFilesDir; - /* package */ File mFile; - private JsonWriter mJsonWriter = NULL_JSON_WRITER; // should never be null + /* package */ File mFilesDir; + /* package */ String mUUIDString; + /* package */ ResearchLog mMainResearchLog; + // The mIntentionalResearchLog records all events for the session, private or not (excepting + // passwords). It is written to permanent storage only if the user explicitly commands + // the system to do so. + /* package */ ResearchLog mIntentionalResearchLog; + // LogUnits are queued here and released only when the user requests the intentional log. + private final List mIntentionalResearchLogQueue = new ArrayList(); - private int mLoggingState; - private static final int LOGGING_STATE_OFF = 0; - private static final int LOGGING_STATE_ON = 1; - private static final int LOGGING_STATE_STOPPING = 2; private boolean mIsPasswordView = false; + private boolean mIsLoggingSuspended = false; // digits entered by the user are replaced with this codepoint. /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = @@ -111,34 +100,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // used to check whether words are not unique private Suggest mSuggest; private Dictionary mDictionary; - - private static class NullOutputStream extends OutputStream { - /** {@inheritDoc} */ - @Override - public void write(byte[] buffer, int offset, int count) { - // nop - } - - /** {@inheritDoc} */ - @Override - public void write(byte[] buffer) { - // nop - } - - @Override - public void write(int oneByte) { - } - } + private KeyboardSwitcher mKeyboardSwitcher; private ResearchLogger() { - mLoggingState = LOGGING_STATE_OFF; } public static ResearchLogger getInstance() { return sInstance; } - public void init(final InputMethodService ims, final SharedPreferences prefs) { + public void init(final InputMethodService ims, final SharedPreferences prefs, + KeyboardSwitcher keyboardSwitcher) { assert ims != null; if (ims == null) { Log.w(TAG, "IMS is null; logging is off"); @@ -149,129 +121,100 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } if (prefs != null) { + mUUIDString = getUUID(prefs); sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); prefs.registerOnSharedPreferenceChangeListener(this); } + mKeyboardSwitcher = keyboardSwitcher; } - public synchronized void start() { - Log.d(TAG, "start called"); + private File createLogFile(File filesDir) { + final StringBuilder sb = new StringBuilder(); + sb.append(FILENAME_PREFIX).append('-'); + sb.append(mUUIDString).append('-'); + sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); + sb.append(FILENAME_SUFFIX); + return new File(filesDir, sb.toString()); + } + + public void start() { if (!sIsLogging) { // Log.w(TAG, "not in usability mode; not logging"); return; } if (mFilesDir == null || !mFilesDir.exists()) { Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); - } else { - if (mHandlerThread == null || !mHandlerThread.isAlive()) { - mHandlerThread = new HandlerThread("ResearchLogger logging task", - Process.THREAD_PRIORITY_BACKGROUND); - mHandlerThread.start(); - mLoggingHandler = null; - mLoggingState = LOGGING_STATE_OFF; - } - if (mLoggingHandler == null) { - mLoggingHandler = new Handler(mHandlerThread.getLooper()); - mLoggingState = LOGGING_STATE_OFF; - } - if (mFile == null) { - final String timestampString = TIMESTAMP_DATEFORMAT.format(new Date()); - mFile = new File(mFilesDir, FILENAME_PREFIX + timestampString + FILENAME_SUFFIX); - } - if (mLoggingState == LOGGING_STATE_OFF) { - try { - mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); - mJsonWriter.setLenient(true); - mJsonWriter.beginArray(); - mLoggingState = LOGGING_STATE_ON; - } catch (IOException e) { - Log.w(TAG, "cannot start JsonWriter"); - mJsonWriter = NULL_JSON_WRITER; - e.printStackTrace(); - } - } + return; } - } - - public synchronized void stop() { - Log.d(TAG, "stop called"); - if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) { - mLoggingState = LOGGING_STATE_STOPPING; - flushEventQueue(true); - // put this in the Handler queue so pending writes are processed first. - mLoggingHandler.post(new Runnable() { - @Override - public void run() { - try { - Log.d(TAG, "closing jsonwriter"); - mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore - e1.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mJsonWriter = NULL_JSON_WRITER; - mFile = null; - mLoggingState = LOGGING_STATE_OFF; - if (DEBUG) { - Log.d(TAG, "logfile closed"); - } - Log.d(TAG, "finished stop(), notifying"); - synchronized (ResearchLogger.this) { - ResearchLogger.this.notify(); - } - } - } - }); - try { - wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } + if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { + mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); } - } - - public synchronized boolean abort() { - Log.d(TAG, "abort called"); - boolean isLogFileDeleted = false; - if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) { - mLoggingState = LOGGING_STATE_STOPPING; - try { - Log.d(TAG, "closing jsonwriter"); - mJsonWriter.endArray(); - mJsonWriter.close(); - } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore - e1.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mJsonWriter = NULL_JSON_WRITER; - // delete file - final boolean isDeleted = mFile.delete(); - if (isDeleted) { - isLogFileDeleted = true; - } - mFile = null; - mLoggingState = LOGGING_STATE_OFF; - if (DEBUG) { - Log.d(TAG, "logfile closed"); - } - } - } - return isLogFileDeleted; - } - - /* package */ synchronized void flush() { try { - mJsonWriter.flush(); + mMainResearchLog.start(); + if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { + mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); + } + mIntentionalResearchLog.start(); } catch (IOException e) { - e.printStackTrace(); + Log.w(TAG, "Could not start ResearchLogger."); + } + } + + public void stop() { + if (mMainResearchLog != null) { + mMainResearchLog.stop(); + } + } + + public boolean abort() { + mIsLoggingSuspended = true; + requestIndicatorRedraw(); + boolean didAbortMainLog = false; + if (mMainResearchLog != null) { + mMainResearchLog.abort(); + try { + mMainResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + } catch (InterruptedException e) { + // interrupted early. carry on. + } + if (mMainResearchLog.isAbortSuccessful()) { + didAbortMainLog = true; + } + } + boolean didAbortIntentionalLog = false; + if (mIntentionalResearchLog != null) { + mIntentionalResearchLog.abort(); + try { + mIntentionalResearchLog.waitUntilStopped(ABORT_TIMEOUT_IN_MS); + } catch (InterruptedException e) { + // interrupted early. carry on. + } + if (mIntentionalResearchLog.isAbortSuccessful()) { + didAbortIntentionalLog = true; + } + } + return didAbortMainLog && didAbortIntentionalLog; + } + + /* package */ void flush() { + if (mMainResearchLog != null) { + mMainResearchLog.flush(); + } + } + + private void logWholeSessionHistory() throws IOException { + try { + LogUnit headerLogUnit = new LogUnit(); + headerLogUnit.addLogAtom(EVENTKEYS_INTENTIONAL_LOG, EVENTKEYS_NULLVALUES, false); + mIntentionalResearchLog.publishAllEvents(headerLogUnit); + for (LogUnit logUnit : mIntentionalResearchLogQueue) { + mIntentionalResearchLog.publishAllEvents(logUnit); + } + mIntentionalResearchLog.stop(); + mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); + mIntentionalResearchLog.start(); + } finally { + mIntentionalResearchLogQueue.clear(); } } @@ -290,7 +233,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final CharSequence title = latinIME.getString(R.string.english_ime_research_log); final CharSequence[] items = new CharSequence[] { latinIME.getString(R.string.note_timestamp_for_researchlog), - latinIME.getString(R.string.do_not_log_this_session), + mIsLoggingSuspended ? latinIME.getString(R.string.enable_session_logging) : + latinIME.getString(R.string.do_not_log_this_session), + latinIME.getString(R.string.log_whole_session_history), }; final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override @@ -298,28 +243,46 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang di.dismiss(); switch (position) { case 0: - ResearchLogger.getInstance().userTimestamp(); + userTimestamp(); Toast.makeText(latinIME, R.string.notify_recorded_timestamp, Toast.LENGTH_LONG).show(); break; case 1: - Toast toast = Toast.makeText(latinIME, - R.string.notify_session_log_deleting, Toast.LENGTH_LONG); - toast.show(); - final ResearchLogger logger = ResearchLogger.getInstance(); - boolean isLogDeleted = logger.abort(); - toast.cancel(); - if (isLogDeleted) { - Toast.makeText(latinIME, R.string.notify_session_log_deleted, - Toast.LENGTH_LONG).show(); + if (mIsLoggingSuspended) { + mIsLoggingSuspended = false; + requestIndicatorRedraw(); + Toast toast = Toast.makeText(latinIME, + R.string.notify_session_logging_enabled, Toast.LENGTH_LONG); } else { - Toast.makeText(latinIME, - R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG) - .show(); + Toast toast = Toast.makeText(latinIME, + R.string.notify_session_log_deleting, Toast.LENGTH_LONG); + toast.show(); + boolean isLogDeleted = abort(); + toast.cancel(); + if (isLogDeleted) { + Toast.makeText(latinIME, R.string.notify_session_log_deleted, + Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(latinIME, + R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG) + .show(); + } + } + break; + case 2: + try { + logWholeSessionHistory(); + Toast.makeText(latinIME, R.string.notify_session_history_logged, + Toast.LENGTH_LONG).show(); + } catch (IOException e) { + Toast.makeText(latinIME, R.string.notify_session_history_not_logged, + Toast.LENGTH_LONG).show(); + e.printStackTrace(); } break; } } + }; final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME) .setItems(items, listener) @@ -336,7 +299,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { - return mLoggingState == LOGGING_STATE_ON && !mIsPasswordView; + return !mIsPasswordView && !mIsLoggingSuspended; + } + + public void requestIndicatorRedraw() { + // invalidate any existing graphics + if (mKeyboardSwitcher != null) { + mKeyboardSwitcher.getKeyboardView().invalidateAllKeys(); + } } private static final String CURRENT_TIME_KEY = "_ct"; @@ -360,7 +330,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys, final Object[] values) { assert values.length + 1 == keys.length; - mCurrentLogUnit.addLogAtom(keys, values, true); + if (isAllowedToLog()) { + mCurrentLogUnit.addLogAtom(keys, values, true); + } } /** @@ -377,7 +349,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang */ private synchronized void enqueueEvent(final String[] keys, final Object[] values) { assert values.length + 1 == keys.length; - mCurrentLogUnit.addLogAtom(keys, values, false); + if (isAllowedToLog()) { + mCurrentLogUnit.addLogAtom(keys, values, false); + } } // Used to track how often words are logged. Too-frequent logging can leak @@ -453,131 +427,33 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private void onWordComplete(String word) { - final boolean isPrivacyThreat = isPrivacyThreat(word); - flushEventQueue(isPrivacyThreat); - if (isPrivacyThreat) { + if (isPrivacyThreat(word)) { + publishLogUnit(mCurrentLogUnit, true); mLoggingFrequencyState.onWordNotLogged(); } else { + publishLogUnit(mCurrentLogUnit, false); mLoggingFrequencyState.onWordLogged(); } - } - - /** - * Write out enqueued LogEvents to the log, possibly dropping privacy sensitive events. - */ - /* package for test */ synchronized void flushEventQueue( - boolean removePotentiallyPrivateEvents) { - if (isAllowedToLog()) { - mCurrentLogUnit.setRemovePotentiallyPrivateEvents(removePotentiallyPrivateEvents); - mLoggingHandler.post(mCurrentLogUnit); - } mCurrentLogUnit = new LogUnit(); } - private synchronized void outputEvent(final String[] keys, final Object[] values) { - try { - mJsonWriter.beginObject(); - mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); - mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis()); - mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]); - final int length = values.length; - for (int i = 0; i < length; i++) { - mJsonWriter.name(keys[i + 1]); - Object value = values[i]; - if (value instanceof String) { - mJsonWriter.value((String) value); - } else if (value instanceof Number) { - mJsonWriter.value((Number) value); - } else if (value instanceof Boolean) { - mJsonWriter.value((Boolean) value); - } else if (value instanceof CompletionInfo[]) { - CompletionInfo[] ci = (CompletionInfo[]) value; - mJsonWriter.beginArray(); - for (int j = 0; j < ci.length; j++) { - mJsonWriter.value(ci[j].toString()); - } - mJsonWriter.endArray(); - } else if (value instanceof SharedPreferences) { - SharedPreferences prefs = (SharedPreferences) value; - mJsonWriter.beginObject(); - for (Map.Entry entry : prefs.getAll().entrySet()) { - mJsonWriter.name(entry.getKey()); - final Object innerValue = entry.getValue(); - if (innerValue == null) { - mJsonWriter.nullValue(); - } else if (innerValue instanceof Boolean) { - mJsonWriter.value((Boolean) innerValue); - } else if (innerValue instanceof Number) { - mJsonWriter.value((Number) innerValue); - } else { - mJsonWriter.value(innerValue.toString()); - } - } - mJsonWriter.endObject(); - } else if (value instanceof Key[]) { - Key[] keyboardKeys = (Key[]) value; - mJsonWriter.beginArray(); - for (Key keyboardKey : keyboardKeys) { - mJsonWriter.beginObject(); - mJsonWriter.name("code").value(keyboardKey.mCode); - mJsonWriter.name("altCode").value(keyboardKey.mAltCode); - mJsonWriter.name("x").value(keyboardKey.mX); - mJsonWriter.name("y").value(keyboardKey.mY); - mJsonWriter.name("w").value(keyboardKey.mWidth); - mJsonWriter.name("h").value(keyboardKey.mHeight); - mJsonWriter.endObject(); - } - mJsonWriter.endArray(); - } else if (value instanceof SuggestedWords) { - SuggestedWords words = (SuggestedWords) value; - mJsonWriter.beginObject(); - mJsonWriter.name("typedWordValid").value(words.mTypedWordValid); - mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect); - mJsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); - mJsonWriter.name("isObsoleteSuggestions") - .value(words.mIsObsoleteSuggestions); - mJsonWriter.name("isPrediction") - .value(words.mIsPrediction); - mJsonWriter.name("words"); - mJsonWriter.beginArray(); - final int size = words.size(); - for (int j = 0; j < size; j++) { - SuggestedWordInfo wordInfo = words.getWordInfo(j); - mJsonWriter.value(wordInfo.toString()); - } - mJsonWriter.endArray(); - mJsonWriter.endObject(); - } else if (value == null) { - mJsonWriter.nullValue(); - } else { - Log.w(TAG, "Unrecognized type to be logged: " + - (value == null ? "" : value.getClass().getName())); - mJsonWriter.nullValue(); - } - } - mJsonWriter.endObject(); - } catch (IOException e) { - e.printStackTrace(); - Log.w(TAG, "Error in JsonWriter; disabling logging"); - try { - mJsonWriter.close(); - } catch (IllegalStateException e1) { - // assume that this is just the json not being terminated properly. - // ignore - } catch (IOException e1) { - e1.printStackTrace(); - } finally { - mJsonWriter = NULL_JSON_WRITER; - } + private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { + if (isPrivacySensitive) { + mMainResearchLog.publishPublicEvents(logUnit); + } else { + mMainResearchLog.publishAllEvents(logUnit); } + mIntentionalResearchLogQueue.add(logUnit); } - private static class LogUnit implements Runnable { + /* package */ void publishCurrentLogUnit(ResearchLog researchLog, boolean isPrivacySensitive) { + publishLogUnit(mCurrentLogUnit, isPrivacySensitive); + } + + static class LogUnit { private final List mKeysList = new ArrayList(); private final List mValuesList = new ArrayList(); private final List mIsPotentiallyPrivate = new ArrayList(); - private boolean mRemovePotentiallyPrivateEvents = true; private void addLogAtom(final String[] keys, final Object[] values, final Boolean isPotentiallyPrivate) { @@ -586,20 +462,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mIsPotentiallyPrivate.add(isPotentiallyPrivate); } - void setRemovePotentiallyPrivateEvents(boolean removePotentiallyPrivateEvents) { - mRemovePotentiallyPrivateEvents = removePotentiallyPrivateEvents; + public void publishPublicEventsTo(ResearchLog researchLog) { + final int size = mKeysList.size(); + for (int i = 0; i < size; i++) { + if (!mIsPotentiallyPrivate.get(i)) { + researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); + } + } } - @Override - public void run() { - final int numAtoms = mKeysList.size(); - for (int atomIndex = 0; atomIndex < numAtoms; atomIndex++) { - if (mRemovePotentiallyPrivateEvents && mIsPotentiallyPrivate.get(atomIndex)) { - continue; - } - final String[] keys = mKeysList.get(atomIndex); - final Object[] values = mValuesList.get(atomIndex); - ResearchLogger.getInstance().outputEvent(keys, values); + public void publishAllEventsTo(ResearchLog researchLog) { + final int size = mKeysList.size(); + for (int i = 0; i < size; i++) { + researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i)); } } } @@ -642,6 +517,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } + private static final String[] EVENTKEYS_INTENTIONAL_LOG = { + "IntentionalLog" + }; private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT = { "LatinKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size", "pressure" @@ -792,7 +670,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); - researchLogger.flushEventQueue(true); // Play it safe. Remove privacy-sensitive events. + // Play it safe. Remove privacy-sensitive events. + researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); + researchLogger.mCurrentLogUnit = new LogUnit(); } } @@ -804,7 +684,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final SharedPreferences prefs) { if (editorInfo != null) { final Object[] values = { - getUUID(prefs), editorInfo.packageName, Integer.toHexString(editorInfo.inputType), + getInstance().mUUIDString, editorInfo.packageName, + Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, OUTPUT_FORMAT_VERSION }; diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index f810eccf4..31566bf13 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -153,17 +153,22 @@ public class Suggest { mAutoCorrectionThreshold = threshold; } - // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder public SuggestedWords getSuggestedWords( final WordComposer wordComposer, CharSequence prevWordForBigram, - final ProximityInfo proximityInfo, final boolean isCorrectionEnabled, - // TODO: remove isPrediction parameter. It effectively means the same thing - // as wordComposer.size() <= 1 - final boolean isPrediction) { + final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) { LatinImeLogger.onStartSuggestion(prevWordForBigram); - final boolean isFirstCharCapitalized = - !isPrediction && wordComposer.isFirstCharCapitalized(); - final boolean isAllUpperCase = !isPrediction && wordComposer.isAllUpperCase(); + if (wordComposer.isBatchMode()) { + return getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo); + } else { + return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo, + isCorrectionEnabled); + } + } + + // Retrieves suggestions for the typing input. + private SuggestedWords getSuggestedWordsForTypingInput( + final WordComposer wordComposer, CharSequence prevWordForBigram, + final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) { final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount(); final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, MAX_SUGGESTIONS); @@ -174,34 +179,20 @@ public class Suggest { : typedWord; LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED); - if (wordComposer.size() <= 1 && isCorrectionEnabled) { - // At first character typed, search only the bigrams - if (!TextUtils.isEmpty(prevWordForBigram)) { - for (final String key : mDictionaries.keySet()) { - final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getBigrams(wordComposer, prevWordForBigram)); - } - } - } else if (wordComposer.size() > 1) { - final WordComposer wordComposerForLookup; - if (trailingSingleQuotesCount > 0) { - wordComposerForLookup = new WordComposer(wordComposer); - for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) { - wordComposerForLookup.deleteLast(); - } - } else { - wordComposerForLookup = wordComposer; - } - // At second character typed, search the unigrams (scores being affected by bigrams) - for (final String key : mDictionaries.keySet()) { - // Skip UserUnigramDictionary and WhitelistDictionary to lookup - if (key.equals(Dictionary.TYPE_USER_HISTORY) - || key.equals(Dictionary.TYPE_WHITELIST)) - continue; - final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getWords( - wordComposerForLookup, prevWordForBigram, proximityInfo)); + final WordComposer wordComposerForLookup; + if (trailingSingleQuotesCount > 0) { + wordComposerForLookup = new WordComposer(wordComposer); + for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) { + wordComposerForLookup.deleteLast(); } + } else { + wordComposerForLookup = wordComposer; + } + + for (final String key : mDictionaries.keySet()) { + final Dictionary dictionary = mDictionaries.get(key); + suggestionsSet.addAll(dictionary.getSuggestions( + wordComposerForLookup, prevWordForBigram, proximityInfo)); } // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid" @@ -214,10 +205,23 @@ public class Suggest { final CharSequence whitelistedWord = mWhiteListDictionary.getWhitelistedWord(consideredWord); + if (whitelistedWord != null) { + // MAX_SCORE ensures this will be considered strong enough to be auto-corrected + suggestionsSet.add(new SuggestedWordInfo(whitelistedWord, + SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST, + Dictionary.TYPE_WHITELIST)); + } final boolean hasAutoCorrection; - if (!isCorrectionEnabled || !allowsToBeAutoCorrected || wordComposer.isMostlyCaps() - || wordComposer.isResumed() || !hasMainDictionary()) { + // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because + // any attempt to do auto-correction is already shielded with a test for this flag; at the + // same time, it feels wrong that the SuggestedWord object includes information about + // the current settings. It may also be useful to know, when the setting is off, whether + // the word *would* have been auto-corrected. + if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord() + || suggestionsSet.isEmpty() + || wordComposer.isMostlyCaps() || wordComposer.isResumed() + || !hasMainDictionary()) { // If we don't have a main dictionary, we never want to auto-correct. The reason for // this is, the user may have a contact whose name happens to match a valid word in // their language, and it will unexpectedly auto-correct. For example, if the user @@ -225,26 +229,16 @@ public class Suggest { // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no // auto-correct. hasAutoCorrection = false; - } else if (null != whitelistedWord) { - hasAutoCorrection = true; - } else if (suggestionsSet.isEmpty()) { - hasAutoCorrection = false; - } else if (AutoCorrection.suggestionExceedsAutoCorrectionThreshold(suggestionsSet.first(), - consideredWord, mAutoCorrectionThreshold)) { - hasAutoCorrection = true; } else { - hasAutoCorrection = false; - } - - if (whitelistedWord != null) { - suggestionsSet.add(new SuggestedWordInfo(whitelistedWord, - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST, - Dictionary.TYPE_WHITELIST)); + hasAutoCorrection = AutoCorrection.suggestionExceedsAutoCorrectionThreshold( + suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold); } final ArrayList suggestionsContainer = new ArrayList(suggestionsSet); final int suggestionsCount = suggestionsContainer.size(); + final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); + final boolean isAllUpperCase = wordComposer.isAllUpperCase(); if (isFirstCharCapitalized || isAllUpperCase || 0 != trailingSingleQuotesCount) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); @@ -278,11 +272,42 @@ public class Suggest { // TODO: this first argument is lying. If this is a whitelisted word which is an // actual word, it says typedWordValid = false, which looks wrong. We should either // rename the attribute or change the value. - !isPrediction && !allowsToBeAutoCorrected /* typedWordValid */, - !isPrediction && hasAutoCorrection, /* willAutoCorrect */ + !allowsToBeAutoCorrected /* typedWordValid */, + hasAutoCorrection, /* willAutoCorrect */ false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, - isPrediction); + !wordComposer.isComposingWord() /* isPrediction */); + } + + // Retrieves suggestions for the batch input. + private SuggestedWords getSuggestedWordsForBatchInput( + final WordComposer wordComposer, CharSequence prevWordForBigram, + final ProximityInfo proximityInfo) { + final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, + MAX_SUGGESTIONS); + + // At second character typed, search the unigrams (scores being affected by bigrams) + for (final String key : mDictionaries.keySet()) { + // Skip UserUnigramDictionary and WhitelistDictionary to lookup + if (key.equals(Dictionary.TYPE_USER_HISTORY) + || key.equals(Dictionary.TYPE_WHITELIST)) { + continue; + } + final Dictionary dictionary = mDictionaries.get(key); + suggestionsSet.addAll(dictionary.getSuggestions( + wordComposer, prevWordForBigram, proximityInfo)); + } + + final ArrayList suggestionsContainer = + new ArrayList(suggestionsSet); + + SuggestedWordInfo.removeDups(suggestionsContainer); + return new SuggestedWords(suggestionsContainer, + true /* typedWordValid */, + true /* willAutoCorrect */, + false /* isPunctuationSuggestions */, + false /* isObsoleteSuggestions */, + false /* isPrediction */); } private static ArrayList getSuggestionsInfoListWithDebugInfo( diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index f079c2112..88fc006df 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -125,6 +125,7 @@ public class SuggestedWords { public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation public static final int KIND_APP_DEFINED = 6; // Suggested by the application public static final int KIND_SHORTCUT = 7; // A shortcut + public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input) public final String mWord; public final int mScore; public final int mKind; // one of the KIND_* constants above diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java index 9b20bd690..bdd988df2 100644 --- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java @@ -32,10 +32,10 @@ public class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryD } @Override - public synchronized ArrayList getWords(final WordComposer codes, + public synchronized ArrayList getSuggestions(final WordComposer codes, final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { syncReloadDictionaryIfRequired(); - return getWordsInner(codes, prevWordForBigrams, proximityInfo); + return super.getSuggestions(codes, prevWordForBigrams, proximityInfo); } @Override diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java index 5b2a6edec..b8cfddd4e 100644 --- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java @@ -35,10 +35,10 @@ public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionar } @Override - public synchronized ArrayList getWords(final WordComposer codes, + public synchronized ArrayList getSuggestions(final WordComposer codes, final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) { syncReloadDictionaryIfRequired(); - return getWordsInner(codes, prevWordForBigrams, proximityInfo); + return super.getSuggestions(codes, prevWordForBigrams, proximityInfo); } @Override diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java index 73fa83f9a..3bb670c9a 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java @@ -27,9 +27,12 @@ import android.os.AsyncTask; import android.provider.BaseColumns; import android.util.Log; +import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams; import java.lang.ref.SoftReference; +import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; @@ -157,6 +160,14 @@ public class UserHistoryDictionary extends ExpandableDictionary { // super.close(); } + @Override + protected ArrayList getWordsInner(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { + // Inhibit suggestions (not predictions) for user history for now. Removing this method + // is enough to use it through the standard ExpandableDictionary way. + return null; + } + /** * Return whether the passed charsequence is in the dictionary. */ diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java index 3af22140e..14476dcf0 100644 --- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java +++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java @@ -22,8 +22,11 @@ import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.LocaleUtils.RunInLocale; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; @@ -88,6 +91,13 @@ public class WhitelistDictionary extends ExpandableDictionary { return null; } + @Override + public ArrayList getSuggestions(final WordComposer composer, + final CharSequence prevWord, final ProximityInfo proximityInfo) { + // Whitelist does not supply any suggestions or predictions. + return null; + } + // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist // lists that word a should autocorrect to word b, and word c would autocorrect to // an upper-cased version of a. In this case, the way this return value is used would diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index bfa41c784..25e29008e 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -37,6 +37,7 @@ public class WordComposer { private final StringBuilder mTypedWord; private CharSequence mAutoCorrection; private boolean mIsResumed; + private boolean mIsBatchMode; // Cache these values for performance private int mCapsCount; @@ -55,6 +56,7 @@ public class WordComposer { mAutoCorrection = null; mTrailingSingleQuotesCount = 0; mIsResumed = false; + mIsBatchMode = false; refreshSize(); } @@ -67,6 +69,7 @@ public class WordComposer { mAutoCapitalized = source.mAutoCapitalized; mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; mIsResumed = source.mIsResumed; + mIsBatchMode = source.mIsBatchMode; refreshSize(); } @@ -80,6 +83,7 @@ public class WordComposer { mIsFirstCharCapitalized = false; mTrailingSingleQuotesCount = 0; mIsResumed = false; + mIsBatchMode = false; refreshSize(); } @@ -140,6 +144,12 @@ public class WordComposer { mAutoCorrection = null; } + // TODO: We may want to have appendBatchInputPointers() as well. + public void setBatchInputPointers(InputPointers batchPointers) { + mInputPointers.copy(batchPointers); + mIsBatchMode = true; + } + /** * Internal method to retrieve reasonable proximity info for a character. */ @@ -295,7 +305,11 @@ public class WordComposer { && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) { lastComposedWord.deactivate(); } + mCapsCount = 0; + mIsBatchMode = false; mTypedWord.setLength(0); + mTrailingSingleQuotesCount = 0; + mIsFirstCharCapitalized = false; refreshSize(); mAutoCorrection = null; mIsResumed = false; @@ -311,4 +325,8 @@ public class WordComposer { mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. mIsResumed = true; } + + public boolean isBatchMode() { + return mIsBatchMode; + } } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 5332c066a..3bdfe1f27 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -20,14 +20,9 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.service.textservice.SpellCheckerService; -import android.text.TextUtils; import android.util.Log; -import android.util.LruCache; -import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SuggestionsInfo; -import android.view.textservice.TextInfo; -import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.BinaryDictionary; import com.android.inputmethod.latin.ContactsBinaryDictionary; @@ -37,12 +32,10 @@ import com.android.inputmethod.latin.DictionaryFactory; import com.android.inputmethod.latin.LocaleUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.StringUtils; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary; import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary; import com.android.inputmethod.latin.UserBinaryDictionary; import com.android.inputmethod.latin.WhitelistDictionary; -import com.android.inputmethod.latin.WordComposer; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -65,9 +58,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; - private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case - private static final int CAPITALIZE_FIRST = 1; // First only - private static final int CAPITALIZE_ALL = 2; // All caps + public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case + public static final int CAPITALIZE_FIRST = 1; // First only + public static final int CAPITALIZE_ALL = 2; // All caps private final static String[] EMPTY_STRING_ARRAY = new String[0]; private Map mDictionaryPools = @@ -91,8 +84,8 @@ public class AndroidSpellCheckerService extends SpellCheckerService public static final int SCRIPT_LATIN = 0; public static final int SCRIPT_CYRILLIC = 1; - private static final String SINGLE_QUOTE = "\u0027"; - private static final String APOSTROPHE = "\u2019"; + public static final String SINGLE_QUOTE = "\u0027"; + public static final String APOSTROPHE = "\u2019"; private static final TreeMap mLanguageToScript; static { // List of the supported languages and their associated script. We won't check @@ -129,7 +122,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); } - private static int getScriptFromLocale(final Locale locale) { + public static int getScriptFromLocale(final Locale locale) { final Integer script = mLanguageToScript.get(locale.getLanguage()); if (null == script) { throw new RuntimeException("We have been called with an unsupported language: \"" @@ -191,20 +184,27 @@ public class AndroidSpellCheckerService extends SpellCheckerService @Override public Session createSession() { - return new AndroidSpellCheckerSession(this); + // Should not refer to AndroidSpellCheckerSession directly considering + // that AndroidSpellCheckerSession may be overlaid. + return AndroidSpellCheckerSessionFactory.newInstance(this); } - private static SuggestionsInfo getNotInDictEmptySuggestions() { + public static SuggestionsInfo getNotInDictEmptySuggestions() { return new SuggestionsInfo(0, EMPTY_STRING_ARRAY); } - private static SuggestionsInfo getInDictEmptySuggestions() { + public static SuggestionsInfo getInDictEmptySuggestions() { return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY, EMPTY_STRING_ARRAY); } + public SuggestionsGatherer newSuggestionsGatherer(final String text, int maxLength) { + return new SuggestionsGatherer( + text, mSuggestionThreshold, mRecommendedThreshold, maxLength); + } + // TODO: remove this class and replace it by storage local to the session. - private static class SuggestionsGatherer { + public static class SuggestionsGatherer { public static class Result { public final String[] mSuggestions; public final boolean mHasRecommendedSuggestions; @@ -396,7 +396,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService }.start(); } - private DictionaryPool getDictionaryPool(final String locale) { + public DictionaryPool getDictionaryPool(final String locale) { DictionaryPool pool = mDictionaryPools.get(locale); if (null == pool) { final Locale localeObject = LocaleUtils.constructLocaleFromString(locale); @@ -447,7 +447,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService } // This method assumes the text is not empty or null. - private static int getCapitalizationType(String text) { + public static int getCapitalizationType(String text) { // If the first char is not uppercase, then the word is either all lower case, // and in either case we return CAPITALIZE_NONE. if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE; @@ -464,379 +464,4 @@ public class AndroidSpellCheckerService extends SpellCheckerService if (1 == capsCount) return CAPITALIZE_FIRST; return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); } - - private static class AndroidSpellCheckerSession extends Session { - // Immutable, but need the locale which is not available in the constructor yet - private DictionaryPool mDictionaryPool; - // Likewise - private Locale mLocale; - // Cache this for performance - private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. - - private final AndroidSpellCheckerService mService; - - private final SuggestionsCache mSuggestionsCache = new SuggestionsCache(); - - private static class SuggestionsParams { - public final String[] mSuggestions; - public final int mFlags; - public SuggestionsParams(String[] suggestions, int flags) { - mSuggestions = suggestions; - mFlags = flags; - } - } - - private static class SuggestionsCache { - private static final char CHAR_DELIMITER = '\uFFFC'; - private static final int MAX_CACHE_SIZE = 50; - private final LruCache mUnigramSuggestionsInfoCache = - new LruCache(MAX_CACHE_SIZE); - - // TODO: Support n-gram input - private static String generateKey(String query, String prevWord) { - if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) { - return query; - } - return query + CHAR_DELIMITER + prevWord; - } - - // TODO: Support n-gram input - public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) { - return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord)); - } - - // TODO: Support n-gram input - public void putSuggestionsToCache( - String query, String prevWord, String[] suggestions, int flags) { - if (suggestions == null || TextUtils.isEmpty(query)) { - return; - } - mUnigramSuggestionsInfoCache.put( - generateKey(query, prevWord), new SuggestionsParams(suggestions, flags)); - } - } - - AndroidSpellCheckerSession(final AndroidSpellCheckerService service) { - mService = service; - } - - @Override - public void onCreate() { - final String localeString = getLocale(); - mDictionaryPool = mService.getDictionaryPool(localeString); - mLocale = LocaleUtils.constructLocaleFromString(localeString); - mScript = getScriptFromLocale(mLocale); - } - - /* - * Returns whether the code point is a letter that makes sense for the specified - * locale for this spell checker. - * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml - * and is limited to EFIGS languages and Russian. - * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters - * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters. - */ - private static boolean isLetterCheckableByLanguage(final int codePoint, - final int script) { - switch (script) { - case SCRIPT_LATIN: - // Our supported latin script dictionaries (EFIGS) at the moment only include - // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode - // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF, - // so the below is a very efficient way to test for it. As for the 0-0x3F, it's - // excluded from isLetter anyway. - return codePoint <= 0x2AF && Character.isLetter(codePoint); - case SCRIPT_CYRILLIC: - // All Cyrillic characters are in the 400~52F block. There are some in the upper - // Unicode range, but they are archaic characters that are not used in modern - // russian and are not used by our dictionary. - return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint); - default: - // Should never come here - throw new RuntimeException("Impossible value of script: " + script); - } - } - - /** - * Finds out whether a particular string should be filtered out of spell checking. - * - * This will loosely match URLs, numbers, symbols. To avoid always underlining words that - * we know we will never recognize, this accepts a script identifier that should be one - * of the SCRIPT_* constants defined above, to rule out quickly characters from very - * different languages. - * - * @param text the string to evaluate. - * @param script the identifier for the script this spell checker recognizes - * @return true if we should filter this text out, false otherwise - */ - private static boolean shouldFilterOut(final String text, final int script) { - if (TextUtils.isEmpty(text) || text.length() <= 1) return true; - - // TODO: check if an equivalent processing can't be done more quickly with a - // compiled regexp. - // Filter by first letter - final int firstCodePoint = text.codePointAt(0); - // Filter out words that don't start with a letter or an apostrophe - if (!isLetterCheckableByLanguage(firstCodePoint, script) - && '\'' != firstCodePoint) return true; - - // Filter contents - final int length = text.length(); - int letterCount = 0; - for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { - final int codePoint = text.codePointAt(i); - // Any word containing a '@' is probably an e-mail address - // Any word containing a '/' is probably either an ad-hoc combination of two - // words or a URI - in either case we don't want to spell check that - if ('@' == codePoint || '/' == codePoint) return true; - if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; - } - // Guestimate heuristic: perform spell checking if at least 3/4 of the characters - // in this word are letters - return (letterCount * 4 < length * 3); - } - - private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote( - TextInfo ti, SentenceSuggestionsInfo ssi) { - final String typedText = ti.getText(); - if (!typedText.contains(SINGLE_QUOTE)) { - return null; - } - final int N = ssi.getSuggestionsCount(); - final ArrayList additionalOffsets = new ArrayList(); - final ArrayList additionalLengths = new ArrayList(); - final ArrayList additionalSuggestionsInfos = - new ArrayList(); - String currentWord = null; - for (int i = 0; i < N; ++i) { - final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i); - final int flags = si.getSuggestionsAttributes(); - if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) { - continue; - } - final int offset = ssi.getOffsetAt(i); - final int length = ssi.getLengthAt(i); - final String subText = typedText.substring(offset, offset + length); - final String prevWord = currentWord; - currentWord = subText; - if (!subText.contains(SINGLE_QUOTE)) { - continue; - } - final String[] splitTexts = subText.split(SINGLE_QUOTE, -1); - if (splitTexts == null || splitTexts.length <= 1) { - continue; - } - final int splitNum = splitTexts.length; - for (int j = 0; j < splitNum; ++j) { - final String splitText = splitTexts[j]; - if (TextUtils.isEmpty(splitText)) { - continue; - } - if (mSuggestionsCache.getSuggestionsFromCache( - splitText, prevWord) == null) { - continue; - } - final int newLength = splitText.length(); - // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO - final int newFlags = 0; - final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY); - newSi.setCookieAndSequence(si.getCookie(), si.getSequence()); - if (DBG) { - Log.d(TAG, "Override and remove old span over: " - + splitText + ", " + offset + "," + newLength); - } - additionalOffsets.add(offset); - additionalLengths.add(newLength); - additionalSuggestionsInfos.add(newSi); - } - } - final int additionalSize = additionalOffsets.size(); - if (additionalSize <= 0) { - return null; - } - final int suggestionsSize = N + additionalSize; - final int[] newOffsets = new int[suggestionsSize]; - final int[] newLengths = new int[suggestionsSize]; - final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize]; - int i; - for (i = 0; i < N; ++i) { - newOffsets[i] = ssi.getOffsetAt(i); - newLengths[i] = ssi.getLengthAt(i); - newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i); - } - for (; i < suggestionsSize; ++i) { - newOffsets[i] = additionalOffsets.get(i - N); - newLengths[i] = additionalLengths.get(i - N); - newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N); - } - return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths); - } - - @Override - public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple( - TextInfo[] textInfos, int suggestionsLimit) { - final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple( - textInfos, suggestionsLimit); - if (retval == null || retval.length != textInfos.length) { - return retval; - } - for (int i = 0; i < retval.length; ++i) { - final SentenceSuggestionsInfo tempSsi = - fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]); - if (tempSsi != null) { - retval[i] = tempSsi; - } - } - return retval; - } - - @Override - public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, - int suggestionsLimit, boolean sequentialWords) { - final int length = textInfos.length; - final SuggestionsInfo[] retval = new SuggestionsInfo[length]; - for (int i = 0; i < length; ++i) { - final String prevWord; - if (sequentialWords && i > 0) { - final String prevWordCandidate = textInfos[i - 1].getText(); - // Note that an empty string would be used to indicate the initial word - // in the future. - prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate; - } else { - prevWord = null; - } - retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit); - retval[i].setCookieAndSequence( - textInfos[i].getCookie(), textInfos[i].getSequence()); - } - return retval; - } - - // Note : this must be reentrant - /** - * Gets a list of suggestions for a specific string. This returns a list of possible - * corrections for the text passed as an argument. It may split or group words, and - * even perform grammatical analysis. - */ - @Override - public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, - final int suggestionsLimit) { - return onGetSuggestions(textInfo, null, suggestionsLimit); - } - - private SuggestionsInfo onGetSuggestions( - final TextInfo textInfo, final String prevWord, final int suggestionsLimit) { - try { - final String inText = textInfo.getText(); - final SuggestionsParams cachedSuggestionsParams = - mSuggestionsCache.getSuggestionsFromCache(inText, prevWord); - if (cachedSuggestionsParams != null) { - if (DBG) { - Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags); - } - return new SuggestionsInfo( - cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions); - } - - if (shouldFilterOut(inText, mScript)) { - DictAndProximity dictInfo = null; - try { - dictInfo = mDictionaryPool.takeOrGetNull(); - if (null == dictInfo) return getNotInDictEmptySuggestions(); - return dictInfo.mDictionary.isValidWord(inText) ? - getInDictEmptySuggestions() : getNotInDictEmptySuggestions(); - } finally { - if (null != dictInfo) { - if (!mDictionaryPool.offer(dictInfo)) { - Log.e(TAG, "Can't re-insert a dictionary into its pool"); - } - } - } - } - final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE); - - // TODO: Don't gather suggestions if the limit is <= 0 unless necessary - final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text, - mService.mSuggestionThreshold, mService.mRecommendedThreshold, - suggestionsLimit); - final WordComposer composer = new WordComposer(); - final int length = text.length(); - for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { - final int codePoint = text.codePointAt(i); - // The getXYForCodePointAndScript method returns (Y << 16) + X - final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript( - codePoint, mScript); - if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) { - composer.add(codePoint, WordComposer.NOT_A_COORDINATE, - WordComposer.NOT_A_COORDINATE); - } else { - composer.add(codePoint, xy & 0xFFFF, xy >> 16); - } - } - - final int capitalizeType = getCapitalizationType(text); - boolean isInDict = true; - DictAndProximity dictInfo = null; - try { - dictInfo = mDictionaryPool.takeOrGetNull(); - if (null == dictInfo) return getNotInDictEmptySuggestions(); - final ArrayList suggestions = dictInfo.mDictionary.getWords( - composer, prevWord, dictInfo.mProximityInfo); - for (final SuggestedWordInfo suggestion : suggestions) { - final String suggestionStr = suggestion.mWord.toString(); - suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0, - suggestionStr.length(), suggestion.mScore); - } - isInDict = dictInfo.mDictionary.isValidWord(text); - if (!isInDict && CAPITALIZE_NONE != capitalizeType) { - // We want to test the word again if it's all caps or first caps only. - // If it's fully down, we already tested it, if it's mixed case, we don't - // want to test a lowercase version of it. - isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale)); - } - } finally { - if (null != dictInfo) { - if (!mDictionaryPool.offer(dictInfo)) { - Log.e(TAG, "Can't re-insert a dictionary into its pool"); - } - } - } - - final SuggestionsGatherer.Result result = suggestionsGatherer.getResults( - capitalizeType, mLocale); - - if (DBG) { - Log.i(TAG, "Spell checking results for " + text + " with suggestion limit " - + suggestionsLimit); - Log.i(TAG, "IsInDict = " + isInDict); - Log.i(TAG, "LooksLikeTypo = " + (!isInDict)); - Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions); - if (null != result.mSuggestions) { - for (String suggestion : result.mSuggestions) { - Log.i(TAG, suggestion); - } - } - } - - final int flags = - (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY - : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) - | (result.mHasRecommendedSuggestions - ? SuggestionsInfoCompatUtils - .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() - : 0); - final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); - mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags); - return retval; - } catch (RuntimeException e) { - // Don't kill the keyboard if there is a bug in the spell checker - if (DBG) { - throw e; - } else { - Log.e(TAG, "Exception while spellcheking: " + e); - return getNotInDictEmptySuggestions(); - } - } - } - } } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java new file mode 100644 index 000000000..501a0e221 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2012 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.spellcheck; + +import android.text.TextUtils; +import android.util.Log; +import android.view.textservice.SentenceSuggestionsInfo; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import java.util.ArrayList; + +public class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession { + private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName(); + private static final boolean DBG = false; + private final static String[] EMPTY_STRING_ARRAY = new String[0]; + + public AndroidSpellCheckerSession(AndroidSpellCheckerService service) { + super(service); + } + + private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti, + SentenceSuggestionsInfo ssi) { + final String typedText = ti.getText(); + if (!typedText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { + return null; + } + final int N = ssi.getSuggestionsCount(); + final ArrayList additionalOffsets = new ArrayList(); + final ArrayList additionalLengths = new ArrayList(); + final ArrayList additionalSuggestionsInfos = + new ArrayList(); + String currentWord = null; + for (int i = 0; i < N; ++i) { + final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i); + final int flags = si.getSuggestionsAttributes(); + if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) { + continue; + } + final int offset = ssi.getOffsetAt(i); + final int length = ssi.getLengthAt(i); + final String subText = typedText.substring(offset, offset + length); + final String prevWord = currentWord; + currentWord = subText; + if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { + continue; + } + final String[] splitTexts = + subText.split(AndroidSpellCheckerService.SINGLE_QUOTE, -1); + if (splitTexts == null || splitTexts.length <= 1) { + continue; + } + final int splitNum = splitTexts.length; + for (int j = 0; j < splitNum; ++j) { + final String splitText = splitTexts[j]; + if (TextUtils.isEmpty(splitText)) { + continue; + } + if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWord) == null) { + continue; + } + final int newLength = splitText.length(); + // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO + final int newFlags = 0; + final SuggestionsInfo newSi = + new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY); + newSi.setCookieAndSequence(si.getCookie(), si.getSequence()); + if (DBG) { + Log.d(TAG, "Override and remove old span over: " + splitText + ", " + + offset + "," + newLength); + } + additionalOffsets.add(offset); + additionalLengths.add(newLength); + additionalSuggestionsInfos.add(newSi); + } + } + final int additionalSize = additionalOffsets.size(); + if (additionalSize <= 0) { + return null; + } + final int suggestionsSize = N + additionalSize; + final int[] newOffsets = new int[suggestionsSize]; + final int[] newLengths = new int[suggestionsSize]; + final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize]; + int i; + for (i = 0; i < N; ++i) { + newOffsets[i] = ssi.getOffsetAt(i); + newLengths[i] = ssi.getLengthAt(i); + newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i); + } + for (; i < suggestionsSize; ++i) { + newOffsets[i] = additionalOffsets.get(i - N); + newLengths[i] = additionalLengths.get(i - N); + newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N); + } + return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths); + } + + @Override + public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, + int suggestionsLimit) { + final SentenceSuggestionsInfo[] retval = + super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit); + if (retval == null || retval.length != textInfos.length) { + return retval; + } + for (int i = 0; i < retval.length; ++i) { + final SentenceSuggestionsInfo tempSsi = + fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]); + if (tempSsi != null) { + retval[i] = tempSsi; + } + } + return retval; + } + + @Override + public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, + int suggestionsLimit, boolean sequentialWords) { + final int length = textInfos.length; + final SuggestionsInfo[] retval = new SuggestionsInfo[length]; + for (int i = 0; i < length; ++i) { + final String prevWord; + if (sequentialWords && i > 0) { + final String prevWordCandidate = textInfos[i - 1].getText(); + // Note that an empty string would be used to indicate the initial word + // in the future. + prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate; + } else { + prevWord = null; + } + retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit); + retval[i].setCookieAndSequence(textInfos[i].getCookie(), + textInfos[i].getSequence()); + } + return retval; + } +} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java new file mode 100644 index 000000000..8eb1eb68e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2012 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.spellcheck; + +import android.service.textservice.SpellCheckerService.Session; + +public abstract class AndroidSpellCheckerSessionFactory { + public static Session newInstance(AndroidSpellCheckerService service) { + return new AndroidSpellCheckerSession(service); + } +} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java new file mode 100644 index 000000000..0171dc06d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2012 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.spellcheck; + +import android.service.textservice.SpellCheckerService.Session; +import android.text.TextUtils; +import android.util.Log; +import android.util.LruCache; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; +import com.android.inputmethod.latin.LocaleUtils; +import com.android.inputmethod.latin.WordComposer; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer; + +import java.util.ArrayList; +import java.util.Locale; + +public abstract class AndroidWordLevelSpellCheckerSession extends Session { + private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName(); + private static final boolean DBG = false; + + // Immutable, but need the locale which is not available in the constructor yet + private DictionaryPool mDictionaryPool; + // Likewise + private Locale mLocale; + // Cache this for performance + private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. + private final AndroidSpellCheckerService mService; + protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache(); + + private static class SuggestionsParams { + public final String[] mSuggestions; + public final int mFlags; + public SuggestionsParams(String[] suggestions, int flags) { + mSuggestions = suggestions; + mFlags = flags; + } + } + + protected static class SuggestionsCache { + private static final char CHAR_DELIMITER = '\uFFFC'; + private static final int MAX_CACHE_SIZE = 50; + private final LruCache mUnigramSuggestionsInfoCache = + new LruCache(MAX_CACHE_SIZE); + + // TODO: Support n-gram input + private static String generateKey(String query, String prevWord) { + if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) { + return query; + } + return query + CHAR_DELIMITER + prevWord; + } + + // TODO: Support n-gram input + public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) { + return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord)); + } + + // TODO: Support n-gram input + public void putSuggestionsToCache( + String query, String prevWord, String[] suggestions, int flags) { + if (suggestions == null || TextUtils.isEmpty(query)) { + return; + } + mUnigramSuggestionsInfoCache.put( + generateKey(query, prevWord), new SuggestionsParams(suggestions, flags)); + } + } + + AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) { + mService = service; + } + + @Override + public void onCreate() { + final String localeString = getLocale(); + mDictionaryPool = mService.getDictionaryPool(localeString); + mLocale = LocaleUtils.constructLocaleFromString(localeString); + mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale); + } + + /* + * Returns whether the code point is a letter that makes sense for the specified + * locale for this spell checker. + * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml + * and is limited to EFIGS languages and Russian. + * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters + * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters. + */ + private static boolean isLetterCheckableByLanguage(final int codePoint, + final int script) { + switch (script) { + case AndroidSpellCheckerService.SCRIPT_LATIN: + // Our supported latin script dictionaries (EFIGS) at the moment only include + // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode + // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF, + // so the below is a very efficient way to test for it. As for the 0-0x3F, it's + // excluded from isLetter anyway. + return codePoint <= 0x2AF && Character.isLetter(codePoint); + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + // All Cyrillic characters are in the 400~52F block. There are some in the upper + // Unicode range, but they are archaic characters that are not used in modern + // russian and are not used by our dictionary. + return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint); + default: + // Should never come here + throw new RuntimeException("Impossible value of script: " + script); + } + } + + /** + * Finds out whether a particular string should be filtered out of spell checking. + * + * This will loosely match URLs, numbers, symbols. To avoid always underlining words that + * we know we will never recognize, this accepts a script identifier that should be one + * of the SCRIPT_* constants defined above, to rule out quickly characters from very + * different languages. + * + * @param text the string to evaluate. + * @param script the identifier for the script this spell checker recognizes + * @return true if we should filter this text out, false otherwise + */ + private static boolean shouldFilterOut(final String text, final int script) { + if (TextUtils.isEmpty(text) || text.length() <= 1) return true; + + // TODO: check if an equivalent processing can't be done more quickly with a + // compiled regexp. + // Filter by first letter + final int firstCodePoint = text.codePointAt(0); + // Filter out words that don't start with a letter or an apostrophe + if (!isLetterCheckableByLanguage(firstCodePoint, script) + && '\'' != firstCodePoint) return true; + + // Filter contents + final int length = text.length(); + int letterCount = 0; + for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + // Any word containing a '@' is probably an e-mail address + // Any word containing a '/' is probably either an ad-hoc combination of two + // words or a URI - in either case we don't want to spell check that + if ('@' == codePoint || '/' == codePoint) return true; + if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; + } + // Guestimate heuristic: perform spell checking if at least 3/4 of the characters + // in this word are letters + return (letterCount * 4 < length * 3); + } + + // Note : this must be reentrant + /** + * Gets a list of suggestions for a specific string. This returns a list of possible + * corrections for the text passed as an argument. It may split or group words, and + * even perform grammatical analysis. + */ + @Override + public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, + final int suggestionsLimit) { + return onGetSuggestions(textInfo, null, suggestionsLimit); + } + + protected SuggestionsInfo onGetSuggestions( + final TextInfo textInfo, final String prevWord, final int suggestionsLimit) { + try { + final String inText = textInfo.getText(); + final SuggestionsParams cachedSuggestionsParams = + mSuggestionsCache.getSuggestionsFromCache(inText, prevWord); + if (cachedSuggestionsParams != null) { + if (DBG) { + Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags); + } + return new SuggestionsInfo( + cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions); + } + + if (shouldFilterOut(inText, mScript)) { + DictAndProximity dictInfo = null; + try { + dictInfo = mDictionaryPool.takeOrGetNull(); + if (null == dictInfo) { + return AndroidSpellCheckerService.getNotInDictEmptySuggestions(); + } + return dictInfo.mDictionary.isValidWord(inText) + ? AndroidSpellCheckerService.getInDictEmptySuggestions() + : AndroidSpellCheckerService.getNotInDictEmptySuggestions(); + } finally { + if (null != dictInfo) { + if (!mDictionaryPool.offer(dictInfo)) { + Log.e(TAG, "Can't re-insert a dictionary into its pool"); + } + } + } + } + final String text = inText.replaceAll( + AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE); + + // TODO: Don't gather suggestions if the limit is <= 0 unless necessary + //final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text, + //mService.mSuggestionThreshold, mService.mRecommendedThreshold, + //suggestionsLimit); + final SuggestionsGatherer suggestionsGatherer = mService.newSuggestionsGatherer( + text, suggestionsLimit); + final WordComposer composer = new WordComposer(); + final int length = text.length(); + for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + // The getXYForCodePointAndScript method returns (Y << 16) + X + final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript( + codePoint, mScript); + if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) { + composer.add(codePoint, WordComposer.NOT_A_COORDINATE, + WordComposer.NOT_A_COORDINATE); + } else { + composer.add(codePoint, xy & 0xFFFF, xy >> 16); + } + } + + final int capitalizeType = AndroidSpellCheckerService.getCapitalizationType(text); + boolean isInDict = true; + DictAndProximity dictInfo = null; + try { + dictInfo = mDictionaryPool.takeOrGetNull(); + if (null == dictInfo) { + return AndroidSpellCheckerService.getNotInDictEmptySuggestions(); + } + final ArrayList suggestions = + dictInfo.mDictionary.getSuggestions(composer, prevWord, + dictInfo.mProximityInfo); + for (final SuggestedWordInfo suggestion : suggestions) { + final String suggestionStr = suggestion.mWord.toString(); + suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0, + suggestionStr.length(), suggestion.mScore); + } + isInDict = dictInfo.mDictionary.isValidWord(text); + if (!isInDict && AndroidSpellCheckerService.CAPITALIZE_NONE != capitalizeType) { + // We want to test the word again if it's all caps or first caps only. + // If it's fully down, we already tested it, if it's mixed case, we don't + // want to test a lowercase version of it. + isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale)); + } + } finally { + if (null != dictInfo) { + if (!mDictionaryPool.offer(dictInfo)) { + Log.e(TAG, "Can't re-insert a dictionary into its pool"); + } + } + } + + final SuggestionsGatherer.Result result = suggestionsGatherer.getResults( + capitalizeType, mLocale); + + if (DBG) { + Log.i(TAG, "Spell checking results for " + text + " with suggestion limit " + + suggestionsLimit); + Log.i(TAG, "IsInDict = " + isInDict); + Log.i(TAG, "LooksLikeTypo = " + (!isInDict)); + Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions); + if (null != result.mSuggestions) { + for (String suggestion : result.mSuggestions) { + Log.i(TAG, suggestion); + } + } + } + + final int flags = + (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY + : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) + | (result.mHasRecommendedSuggestions + ? SuggestionsInfoCompatUtils + .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() + : 0); + final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); + mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags); + return retval; + } catch (RuntimeException e) { + // Don't kill the keyboard if there is a bug in the spell checker + if (DBG) { + throw e; + } else { + Log.e(TAG, "Exception while spellcheking: " + e); + return AndroidSpellCheckerService.getNotInDictEmptySuggestions(); + } + } + } +} diff --git a/native/jni/Android.mk b/native/jni/Android.mk index a7486ae90..54f61d90d 100644 --- a/native/jni/Android.mk +++ b/native/jni/Android.mk @@ -68,9 +68,10 @@ endif # FLAG_DO_PROFILE LOCAL_MODULE := libjni_latinime_common_static LOCAL_MODULE_TAGS := optional -ifdef ANDROID_BUILD_TOP # In the platform build system +# TODO: Remove this conditional block once we have no issues with building against NDK +ifndef TARGET_BUILD_APPS # A full system image build include external/stlport/libstlport.mk -else # In the unbundled build system +else # An unbundled build LOCAL_NDK_VERSION := 7 LOCAL_SDK_VERSION := 14 LOCAL_NDK_STL_VARIANT := stlport_static @@ -96,9 +97,10 @@ endif # FLAG_DO_PROFILE LOCAL_MODULE := libjni_latinime LOCAL_MODULE_TAGS := optional -ifdef ANDROID_BUILD_TOP # In the platform build system +# TODO: Remove this conditional block once we have no issues with building against NDK +ifndef TARGET_BUILD_APPS # A full system image build LOCAL_STATIC_LIBRARIES += libstlport_static -else # In the unbundled build system +else # An unbundled build LOCAL_NDK_VERSION := 7 LOCAL_SDK_VERSION := 14 LOCAL_NDK_STL_VARIANT := stlport_static diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp index bee0662ee..0a282b865 100644 --- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp +++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp @@ -46,7 +46,8 @@ void releaseDictBuf(void* dictBuf, const size_t length, int fd); static jlong latinime_BinaryDictionary_open(JNIEnv *env, jobject object, jstring sourceDir, jlong dictOffset, jlong dictSize, - jint typedLetterMultiplier, jint fullWordMultiplier, jint maxWordLength, jint maxWords) { + jint typedLetterMultiplier, jint fullWordMultiplier, jint maxWordLength, jint maxWords, + jint maxPredictions) { PROF_OPEN; PROF_START(66); const char *sourceDirChars = env->GetStringUTFChars(sourceDir, 0); @@ -119,7 +120,7 @@ static jlong latinime_BinaryDictionary_open(JNIEnv *env, jobject object, #endif // USE_MMAP_FOR_DICTIONARY } else { dictionary = new Dictionary(dictBuf, dictSize, fd, adjust, typedLetterMultiplier, - fullWordMultiplier, maxWordLength, maxWords); + fullWordMultiplier, maxWordLength, maxWords, maxPredictions); } PROF_END(66); PROF_CLOSE; @@ -131,7 +132,7 @@ static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jobject object, jintArray timesArray, jintArray pointerIdArray, jintArray inputArray, jint arraySize, jint commitPoint, jboolean isGesture, jintArray prevWordForBigrams, jboolean useFullEditDistance, jcharArray outputArray, - jintArray frequencyArray, jintArray spaceIndexArray) { + jintArray frequencyArray, jintArray spaceIndexArray, jintArray outputTypesArray) { Dictionary *dictionary = (Dictionary*) dict; if (!dictionary) return 0; ProximityInfo *pInfo = (ProximityInfo*)proximityInfo; @@ -143,16 +144,26 @@ static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jobject object, int *inputCodes = env->GetIntArrayElements(inputArray, 0); jchar *outputChars = env->GetCharArrayElements(outputArray, 0); int *spaceIndices = env->GetIntArrayElements(spaceIndexArray, 0); + int *outputTypes = env->GetIntArrayElements(outputTypesArray, 0); jint *prevWordChars = prevWordForBigrams ? env->GetIntArrayElements(prevWordForBigrams, 0) : 0; jsize prevWordLength = prevWordChars ? env->GetArrayLength(prevWordForBigrams) : 0; - int count = dictionary->getSuggestions(pInfo, xCoordinates, yCoordinates, times, pointerIds, - inputCodes, arraySize, prevWordChars, prevWordLength, commitPoint, isGesture, - useFullEditDistance, (unsigned short*) outputChars, - frequencies, spaceIndices); + + int count; + if (isGesture || arraySize > 1) { + count = dictionary->getSuggestions(pInfo, xCoordinates, yCoordinates, times, pointerIds, + inputCodes, arraySize, prevWordChars, prevWordLength, commitPoint, isGesture, + useFullEditDistance, (unsigned short*) outputChars, frequencies, spaceIndices, + outputTypes); + } else { + count = dictionary->getBigrams(prevWordChars, prevWordLength, inputCodes, + arraySize, (unsigned short*) outputChars, frequencies, outputTypes); + } + if (prevWordChars) { env->ReleaseIntArrayElements(prevWordForBigrams, prevWordChars, JNI_ABORT); } + env->ReleaseIntArrayElements(outputTypesArray, outputTypes, 0); env->ReleaseIntArrayElements(spaceIndexArray, spaceIndices, 0); env->ReleaseCharArrayElements(outputArray, outputChars, 0); env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT); @@ -164,24 +175,6 @@ static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jobject object, return count; } -static int latinime_BinaryDictionary_getBigrams(JNIEnv *env, jobject object, jlong dict, - jintArray prevWordArray, jint prevWordLength, jintArray inputArray, jint inputArraySize, - jcharArray outputArray, jintArray frequencyArray, jint maxWordLength, jint maxBigrams) { - Dictionary *dictionary = (Dictionary*)dict; - if (!dictionary) return 0; - jint *prevWord = env->GetIntArrayElements(prevWordArray, 0); - int *inputCodes = env->GetIntArrayElements(inputArray, 0); - jchar *outputChars = env->GetCharArrayElements(outputArray, 0); - int *frequencies = env->GetIntArrayElements(frequencyArray, 0); - int count = dictionary->getBigrams(prevWord, prevWordLength, inputCodes, - inputArraySize, (unsigned short*) outputChars, frequencies, maxWordLength, maxBigrams); - env->ReleaseIntArrayElements(frequencyArray, frequencies, 0); - env->ReleaseCharArrayElements(outputArray, outputChars, 0); - env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT); - env->ReleaseIntArrayElements(prevWordArray, prevWord, JNI_ABORT); - return count; -} - static jint latinime_BinaryDictionary_getFrequency(JNIEnv *env, jobject object, jlong dict, jintArray wordArray, jint wordLength) { Dictionary *dictionary = (Dictionary*)dict; @@ -258,13 +251,12 @@ void releaseDictBuf(void* dictBuf, const size_t length, int fd) { } static JNINativeMethod sMethods[] = { - {"openNative", "(Ljava/lang/String;JJIIII)J", (void*)latinime_BinaryDictionary_open}, + {"openNative", "(Ljava/lang/String;JJIIIII)J", (void*)latinime_BinaryDictionary_open}, {"closeNative", "(J)V", (void*)latinime_BinaryDictionary_close}, - {"getSuggestionsNative", "(JJ[I[I[I[I[IIIZ[IZ[C[I[I)I", + {"getSuggestionsNative", "(JJ[I[I[I[I[IIIZ[IZ[C[I[I[I)I", (void*) latinime_BinaryDictionary_getSuggestions}, {"getFrequencyNative", "(J[II)I", (void*)latinime_BinaryDictionary_getFrequency}, {"isValidBigramNative", "(J[I[I)Z", (void*)latinime_BinaryDictionary_isValidBigram}, - {"getBigramsNative", "(J[II[II[C[III)I", (void*)latinime_BinaryDictionary_getBigrams}, {"calcNormalizedScoreNative", "([CI[CII)F", (void*)latinime_BinaryDictionary_calcNormalizedScore}, {"editDistanceNative", "([CI[CI)I", (void*)latinime_BinaryDictionary_editDistance} diff --git a/native/jni/src/bigram_dictionary.cpp b/native/jni/src/bigram_dictionary.cpp index 3bfbfad25..8057e410a 100644 --- a/native/jni/src/bigram_dictionary.cpp +++ b/native/jni/src/bigram_dictionary.cpp @@ -27,8 +27,8 @@ namespace latinime { -BigramDictionary::BigramDictionary(const unsigned char *dict, int maxWordLength) - : DICT(dict), MAX_WORD_LENGTH(maxWordLength) { +BigramDictionary::BigramDictionary(const unsigned char *dict, int maxWordLength, int maxPredictions) + : DICT(dict), MAX_WORD_LENGTH(maxWordLength), MAX_PREDICTIONS(maxPredictions) { if (DEBUG_DICT) { AKLOGI("BigramDictionary - constructor"); } @@ -38,7 +38,7 @@ BigramDictionary::~BigramDictionary() { } bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequency, - const int maxBigrams, int *bigramFreq, unsigned short *bigramChars) const { + int *bigramFreq, unsigned short *bigramChars, int *outputTypes) const { word[length] = 0; if (DEBUG_DICT) { #ifdef FLAG_DBG @@ -50,7 +50,7 @@ bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequ // Find the right insertion point int insertAt = 0; - while (insertAt < maxBigrams) { + while (insertAt < MAX_PREDICTIONS) { if (frequency > bigramFreq[insertAt] || (bigramFreq[insertAt] == frequency && length < Dictionary::wideStrLen(bigramChars + insertAt * MAX_WORD_LENGTH))) { break; @@ -58,16 +58,17 @@ bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequ insertAt++; } if (DEBUG_DICT) { - AKLOGI("Bigram: InsertAt -> %d maxBigrams: %d", insertAt, maxBigrams); + AKLOGI("Bigram: InsertAt -> %d MAX_PREDICTIONS: %d", insertAt, MAX_PREDICTIONS); } - if (insertAt < maxBigrams) { + if (insertAt < MAX_PREDICTIONS) { memmove((char*) bigramFreq + (insertAt + 1) * sizeof(bigramFreq[0]), (char*) bigramFreq + insertAt * sizeof(bigramFreq[0]), - (maxBigrams - insertAt - 1) * sizeof(bigramFreq[0])); + (MAX_PREDICTIONS - insertAt - 1) * sizeof(bigramFreq[0])); bigramFreq[insertAt] = frequency; + outputTypes[insertAt] = Dictionary::KIND_PREDICTION; memmove((char*) bigramChars + (insertAt + 1) * MAX_WORD_LENGTH * sizeof(short), (char*) bigramChars + (insertAt ) * MAX_WORD_LENGTH * sizeof(short), - (maxBigrams - insertAt - 1) * sizeof(short) * MAX_WORD_LENGTH); + (MAX_PREDICTIONS - insertAt - 1) * sizeof(short) * MAX_WORD_LENGTH); unsigned short *dest = bigramChars + (insertAt ) * MAX_WORD_LENGTH; while (length--) { *dest++ = *word++; @@ -88,8 +89,7 @@ bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequ * codesSize: the size of the codes array. * bigramChars: an array for output, at the same format as outwords for getSuggestions. * bigramFreq: an array to output frequencies. - * maxWordLength: the maximum size of a word. - * maxBigrams: the maximum number of bigrams fitting in the bigramChars array. + * outputTypes: an array to output types. * This method returns the number of bigrams this word has, for backward compatibility. * Note: this is not the number of bigrams output in the array, which is the number of * bigrams this word has WHOSE first letter also matches the letter the user typed. @@ -99,8 +99,7 @@ bool BigramDictionary::addWordBigram(unsigned short *word, int length, int frequ * reduce their scope to the ones that match the first letter. */ int BigramDictionary::getBigrams(const int32_t *prevWord, int prevWordLength, int *inputCodes, - int codesSize, unsigned short *bigramChars, int *bigramFreq, int maxWordLength, - int maxBigrams) const { + int codesSize, unsigned short *bigramChars, int *bigramFreq, int *outputTypes) const { // TODO: remove unused arguments, and refrain from storing stuff in members of this class // TODO: have "in" arguments before "out" ones, and make out args explicit in the name @@ -136,8 +135,8 @@ int BigramDictionary::getBigrams(const int32_t *prevWord, int prevWordLength, in // here, but it can't get too bad. const int frequency = BinaryFormat::computeFrequencyForBigram(unigramFreq, bigramFreqTemp); - if (addWordBigram( - bigramBuffer, length, frequency, maxBigrams, bigramFreq, bigramChars)) { + if (addWordBigram(bigramBuffer, length, frequency, bigramFreq, bigramChars, + outputTypes)) { ++bigramCount; } } diff --git a/native/jni/src/bigram_dictionary.h b/native/jni/src/bigram_dictionary.h index 5372276cd..0b3577ad8 100644 --- a/native/jni/src/bigram_dictionary.h +++ b/native/jni/src/bigram_dictionary.h @@ -27,9 +27,9 @@ namespace latinime { class Dictionary; class BigramDictionary { public: - BigramDictionary(const unsigned char *dict, int maxWordLength); + BigramDictionary(const unsigned char *dict, int maxWordLength, int maxPredictions); int getBigrams(const int32_t *word, int length, int *inputCodes, int codesSize, - unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams) const; + unsigned short *outWords, int *frequencies, int *outputTypes) const; int getBigramListPositionForWord(const int32_t *prevWord, const int prevWordLength, const bool forceLowerCaseSearch) const; void fillBigramAddressToFrequencyMapAndFilter(const int32_t *prevWord, const int prevWordLength, @@ -38,8 +38,8 @@ class BigramDictionary { ~BigramDictionary(); private: DISALLOW_IMPLICIT_CONSTRUCTORS(BigramDictionary); - bool addWordBigram(unsigned short *word, int length, int frequency, const int maxBigrams, - int *bigramFreq, unsigned short *bigramChars) const; + bool addWordBigram(unsigned short *word, int length, int frequency, + int *bigramFreq, unsigned short *bigramChars, int *outputTypes) const; int getBigramAddress(int *pos, bool advance); int getBigramFreq(int *pos); void searchForTerminalNode(int addressLookingFor, int frequency); @@ -49,6 +49,7 @@ class BigramDictionary { const unsigned char *DICT; const int MAX_WORD_LENGTH; + const int MAX_PREDICTIONS; // TODO: Re-implement proximity correction for bigram correction static const int MAX_ALTERNATIVES = 1; }; diff --git a/native/jni/src/dictionary.cpp b/native/jni/src/dictionary.cpp index 628a16933..f3166e75a 100644 --- a/native/jni/src/dictionary.cpp +++ b/native/jni/src/dictionary.cpp @@ -29,7 +29,7 @@ namespace latinime { // TODO: Change the type of all keyCodes to uint32_t Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, int typedLetterMultiplier, int fullWordMultiplier, - int maxWordLength, int maxWords) + int maxWordLength, int maxWords, int maxPredictions) : mDict((unsigned char*) dict), mDictSize(dictSize), mMmapFd(mmapFd), mDictBufAdjust(dictBufAdjust) { if (DEBUG_DICT) { @@ -43,7 +43,7 @@ Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, const unsigned int options = BinaryFormat::getFlags(mDict); mUnigramDictionary = new UnigramDictionary(mDict + headerSize, typedLetterMultiplier, fullWordMultiplier, maxWordLength, maxWords, options); - mBigramDictionary = new BigramDictionary(mDict + headerSize, maxWordLength); + mBigramDictionary = new BigramDictionary(mDict + headerSize, maxWordLength, maxPredictions); mGestureDecoder = new GestureDecoderWrapper(maxWordLength, maxWords); mGestureDecoder->setDict(mUnigramDictionary, mBigramDictionary, mDict + headerSize /* dict root */, 0 /* root pos */); diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h index 431f10337..7911403dc 100644 --- a/native/jni/src/dictionary.h +++ b/native/jni/src/dictionary.h @@ -31,20 +31,31 @@ namespace latinime { class Dictionary { public: + // Taken from SuggestedWords.java + const static int KIND_TYPED = 0; // What user typed + const static int KIND_CORRECTION = 1; // Simple correction/suggestion + const static int KIND_COMPLETION = 2; // Completion (suggestion with appended chars) + const static int KIND_WHITELIST = 3; // Whitelisted word + const static int KIND_BLACKLIST = 4; // Blacklisted word + const static int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation + const static int KIND_APP_DEFINED = 6; // Suggested by the application + const static int KIND_SHORTCUT = 7; // A shortcut + const static int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input) + Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust, int typedLetterMultipler, - int fullWordMultiplier, int maxWordLength, int maxWords); + int fullWordMultiplier, int maxWordLength, int maxWords, int maxPredictions); int getSuggestions(ProximityInfo *proximityInfo, int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *codes, int codesSize, int *prevWordChars, int prevWordLength, int commitPoint, bool isGesture, bool useFullEditDistance, unsigned short *outWords, - int *frequencies, int *spaceIndices) { + int *frequencies, int *spaceIndices, int *outputTypes) { int result = 0; if (isGesture) { mGestureDecoder->setPrevWord(prevWordChars, prevWordLength); result = mGestureDecoder->getSuggestions(proximityInfo, xcoordinates, ycoordinates, times, pointerIds, codes, codesSize, commitPoint, - outWords, frequencies, spaceIndices); + outWords, frequencies, spaceIndices, outputTypes); return result; } else { std::map bigramMap; @@ -53,15 +64,16 @@ class Dictionary { prevWordLength, &bigramMap, bigramFilter); result = mUnigramDictionary->getSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, codesSize, &bigramMap, bigramFilter, - useFullEditDistance, outWords, frequencies); + useFullEditDistance, outWords, frequencies, outputTypes); return result; } } int getBigrams(const int32_t *word, int length, int *codes, int codesSize, - unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams) const { + unsigned short *outWords, int *frequencies, int *outputTypes) const { + if (length <= 0) return 0; return mBigramDictionary->getBigrams(word, length, codes, codesSize, outWords, frequencies, - maxWordLength, maxBigrams); + outputTypes); } int getFrequency(const int32_t *word, int length) const; diff --git a/native/jni/src/gesture/gesture_decoder_wrapper.h b/native/jni/src/gesture/gesture_decoder_wrapper.h index 35982f03d..03c84b5fd 100644 --- a/native/jni/src/gesture/gesture_decoder_wrapper.h +++ b/native/jni/src/gesture/gesture_decoder_wrapper.h @@ -39,13 +39,13 @@ class GestureDecoderWrapper : public IncrementalDecoderInterface { int getSuggestions(ProximityInfo *pInfo, int *inputXs, int *inputYs, int *times, int *pointerIds, int *codes, int inputSize, int commitPoint, - unsigned short *outWords, int *frequencies, int *outputIndices) { + unsigned short *outWords, int *frequencies, int *outputIndices, int *outputTypes) { if (!mIncrementalDecoderInterface) { return 0; } return mIncrementalDecoderInterface->getSuggestions( pInfo, inputXs, inputYs, times, pointerIds, codes, inputSize, commitPoint, - outWords, frequencies, outputIndices); + outWords, frequencies, outputIndices, outputTypes); } void reset() { diff --git a/native/jni/src/gesture/incremental_decoder_interface.h b/native/jni/src/gesture/incremental_decoder_interface.h index 957f1ebbe..6d2e273da 100644 --- a/native/jni/src/gesture/incremental_decoder_interface.h +++ b/native/jni/src/gesture/incremental_decoder_interface.h @@ -30,7 +30,7 @@ class IncrementalDecoderInterface { public: virtual int getSuggestions(ProximityInfo *pInfo, int *inputXs, int *inputYs, int *times, int *pointerIds, int *codes, int inputSize, int commitPoint, - unsigned short *outWords, int *frequencies, int *outputIndices) = 0; + unsigned short *outWords, int *frequencies, int *outputIndices, int *outputTypes) = 0; virtual void reset() = 0; virtual void setDict(const UnigramDictionary *dict, const BigramDictionary *bigram, const uint8_t *dictRoot, int rootPos) = 0; diff --git a/native/jni/src/unigram_dictionary.cpp b/native/jni/src/unigram_dictionary.cpp index 22f1657ef..0ffb3eb63 100644 --- a/native/jni/src/unigram_dictionary.cpp +++ b/native/jni/src/unigram_dictionary.cpp @@ -173,7 +173,8 @@ int UnigramDictionary::getSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates, const int *codes, const int codesSize, const std::map *bigramMap, const uint8_t *bigramFilter, - const bool useFullEditDistance, unsigned short *outWords, int *frequencies) const { + const bool useFullEditDistance, unsigned short *outWords, int *frequencies, + int *outputTypes) const { WordsPriorityQueuePool queuePool(MAX_WORDS, SUB_QUEUE_MAX_WORDS, MAX_WORD_LENGTH); queuePool.clearAll(); diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h index 8352c5494..ac14fc0bc 100644 --- a/native/jni/src/unigram_dictionary.h +++ b/native/jni/src/unigram_dictionary.h @@ -81,7 +81,7 @@ class UnigramDictionary { ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates, const int *codes, const int codesSize, const std::map *bigramMap, const uint8_t *bigramFilter, const bool useFullEditDistance, unsigned short *outWords, - int *frequencies) const; + int *frequencies, int *outputTypes) const; virtual ~UnigramDictionary(); private: diff --git a/tests/src/com/android/inputmethod/latin/InputPointersTests.java b/tests/src/com/android/inputmethod/latin/InputPointersTests.java new file mode 100644 index 000000000..524921e25 --- /dev/null +++ b/tests/src/com/android/inputmethod/latin/InputPointersTests.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import android.test.AndroidTestCase; + +public class InputPointersTests extends AndroidTestCase { + public void testNewInstance() { + final InputPointers src = new InputPointers(); + assertEquals("newInstance size", 0, src.getPointerSize()); + assertNotNull("new instance xCoordinates", src.getXCoordinates()); + assertNotNull("new instance yCoordinates", src.getYCoordinates()); + assertNotNull("new instance pointerIds", src.getPointerIds()); + assertNotNull("new instance times", src.getTimes()); + } + + public void testReset() { + final InputPointers src = new InputPointers(); + final int[] xCoordinates = src.getXCoordinates(); + final int[] yCoordinates = src.getXCoordinates(); + final int[] pointerIds = src.getXCoordinates(); + final int[] times = src.getXCoordinates(); + + src.reset(); + assertEquals("after reset size", 0, src.getPointerSize()); + assertNotSame("after reset xCoordinates", xCoordinates, src.getXCoordinates()); + assertNotSame("after reset yCoordinates", yCoordinates, src.getYCoordinates()); + assertNotSame("after reset pointerIds", pointerIds, src.getPointerIds()); + assertNotSame("after reset times", times, src.getTimes()); + } + + public void testAdd() { + final InputPointers src = new InputPointers(); + final int limit = src.getXCoordinates().length * 2 + 10; + for (int i = 0; i < limit; i++) { + src.addPointer(i, i * 2, i * 3, i * 4); + assertEquals("after add " + i, i + 1, src.getPointerSize()); + } + for (int i = 0; i < limit; i++) { + assertEquals("xCoordinates at " + i, i, src.getXCoordinates()[i]); + assertEquals("yCoordinates at " + i, i * 2, src.getYCoordinates()[i]); + assertEquals("pointerIds at " + i, i * 3, src.getPointerIds()[i]); + assertEquals("times at " + i, i * 4, src.getTimes()[i]); + } + } + + public void testAddAt() { + final InputPointers src = new InputPointers(); + final int limit = 1000, step = 100; + for (int i = 0; i < limit; i += step) { + src.addPointer(i, i, i * 2, i * 3, i * 4); + assertEquals("after add at " + i, i + 1, src.getPointerSize()); + } + for (int i = 0; i < limit; i += step) { + assertEquals("xCoordinates at " + i, i, src.getXCoordinates()[i]); + assertEquals("yCoordinates at " + i, i * 2, src.getYCoordinates()[i]); + assertEquals("pointerIds at " + i, i * 3, src.getPointerIds()[i]); + assertEquals("times at " + i, i * 4, src.getTimes()[i]); + } + } + + public void testSet() { + final InputPointers src = new InputPointers(); + final int limit = src.getXCoordinates().length * 2 + 10; + for (int i = 0; i < limit; i++) { + src.addPointer(i, i * 2, i * 3, i * 4); + } + final InputPointers dst = new InputPointers(); + dst.set(src); + assertEquals("after set size", dst.getPointerSize(), src.getPointerSize()); + assertSame("after set xCoordinates", dst.getXCoordinates(), src.getXCoordinates()); + assertSame("after set yCoordinates", dst.getYCoordinates(), src.getYCoordinates()); + assertSame("after set pointerIds", dst.getPointerIds(), src.getPointerIds()); + assertSame("after set times", dst.getTimes(), src.getTimes()); + } + + public void testCopy() { + final InputPointers src = new InputPointers(); + final int limit = 100; + for (int i = 0; i < limit; i++) { + src.addPointer(i, i * 2, i * 3, i * 4); + } + final InputPointers dst = new InputPointers(); + dst.copy(src); + assertEquals("after copy size", dst.getPointerSize(), src.getPointerSize()); + assertNotSame("after copy xCoordinates", dst.getXCoordinates(), src.getXCoordinates()); + assertNotSame("after copy yCoordinates", dst.getYCoordinates(), src.getYCoordinates()); + assertNotSame("after copy pointerIds", dst.getPointerIds(), src.getPointerIds()); + assertNotSame("after copy times", dst.getTimes(), src.getTimes()); + final int size = dst.getPointerSize(); + assertArrayEquals("after copy xCoordinates values", + dst.getXCoordinates(), 0, src.getXCoordinates(), 0, size); + assertArrayEquals("after copy yCoordinates values", + dst.getYCoordinates(), 0, src.getYCoordinates(), 0, size); + assertArrayEquals("after copy pointerIds values", + dst.getPointerIds(), 0, src.getPointerIds(), 0, size); + assertArrayEquals("after copy times values", + dst.getTimes(), 0, src.getTimes(), 0, size); + } + + public void testAppend() { + final InputPointers src = new InputPointers(); + final int srcLen = 100; + for (int i = 0; i < srcLen; i++) { + src.addPointer(i, i * 2, i * 3, i * 4); + } + final int dstLen = 50; + final InputPointers dst = new InputPointers(); + for (int i = 0; i < dstLen; i++) { + final int value = -i - 1; + dst.addPointer(value * 4, value * 3, value * 2, value); + } + final InputPointers dstCopy = new InputPointers(); + dstCopy.copy(dst); + + dst.append(src, 0, 0); + assertEquals("after append zero size", dstLen, dst.getPointerSize()); + assertArrayEquals("after append zero xCoordinates", dstCopy.getXCoordinates(), 0, + dst.getXCoordinates(), 0, dstLen); + assertArrayEquals("after append zero yCoordinates", dstCopy.getYCoordinates(), 0, + dst.getYCoordinates(), 0, dstLen); + assertArrayEquals("after append zero pointerIds", dstCopy.getPointerIds(), 0, + dst.getPointerIds(), 0, dstLen); + assertArrayEquals("after append zero times", dstCopy.getTimes(), 0, + dst.getTimes(), 0, dstLen); + + dst.append(src, 0, srcLen); + assertEquals("after append size", dstLen + srcLen, dst.getPointerSize()); + assertTrue("after append size primitive length", + dst.getPointerIds().length >= dstLen + srcLen); + assertArrayEquals("after append xCoordinates", dstCopy.getXCoordinates(), 0, + dst.getXCoordinates(), 0, dstLen); + assertArrayEquals("after append yCoordinates", dstCopy.getYCoordinates(), 0, + dst.getYCoordinates(), 0, dstLen); + assertArrayEquals("after append pointerIds", dstCopy.getPointerIds(), 0, + dst.getPointerIds(), 0, dstLen); + assertArrayEquals("after append times", dstCopy.getTimes(), 0, + dst.getTimes(), 0, dstLen); + assertArrayEquals("after append xCoordinates", dst.getXCoordinates(), dstLen, + src.getXCoordinates(), 0, srcLen); + assertArrayEquals("after append yCoordinates", dst.getYCoordinates(), dstLen, + src.getYCoordinates(), 0, srcLen); + assertArrayEquals("after append pointerIds", dst.getPointerIds(), dstLen, + src.getPointerIds(), 0, srcLen); + assertArrayEquals("after append times", dst.getTimes(), dstLen, + src.getTimes(), 0, srcLen); + } + + private static void assertArrayEquals(String message, int[] expecteds, int expectedPos, + int[] actuals, int actualPos, int length) { + if (expecteds == null && actuals == null) { + return; + } + if (expecteds == null || actuals == null) { + fail(message + ": expecteds=" + expecteds + " actuals=" + actuals); + } + for (int i = 0; i < length; i++) { + assertEquals(message + ": element at " + i, + expecteds[i + expectedPos], actuals[i + actualPos]); + } + } +}