convert keyboard project to Android Studio / Gradle project

This commit is contained in:
Philipp Crocoll 2016-01-13 21:37:41 +01:00
parent 7b0c4c52dd
commit bc0659957f
528 changed files with 35063 additions and 0 deletions

View File

@ -0,0 +1 @@
#Wed Jan 13 21:02:12 CET 2016

View File

@ -0,0 +1 @@
java

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

View File

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="LOCAL" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="C:\Program Files\Android\Android Studio\gradle\gradle-2.2.1" />
<option name="gradleJvm" value="1.7" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
<component name="masterDetails">
<states>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>1.7</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/KP2ASoftkeyboard_AS.iml" filepath="$PROJECT_DIR$/KP2ASoftkeyboard_AS.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="" />
</component>
</project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="KP2ASoftkeyboard_AS" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="jdk" jdkName="1.7" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="KP2ASoftkeyboard_AS" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="debug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
<option name="LIBRARY_PROJECT" value="true" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,18 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.0"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}

View File

@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="keepass2android.softkeyboard">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-sdk android:targetSdkVersion="14" android:minSdkVersion="14"/>
<application android:label="MyKeyboard"
android:killAfterRestore="false">
</application>
</manifest>

View File

@ -0,0 +1,110 @@
package keepass2android.kbbridge;
import java.util.List;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
public class ImeSwitcher {
private static final String SECURE_SETTINGS_PACKAGE_NAME = "com.intangibleobject.securesettings.plugin";
private static final String PREVIOUS_KEYBOARD = "previous_keyboard";
private static final String KP2A_SWITCHER = "KP2A_Switcher";
private static final String Tag = "KP2A_SWITCHER";
public static void switchToPreviousKeyboard(InputMethodService ims, boolean silent)
{
try {
InputMethodManager imm = (InputMethodManager) ims.getSystemService(Context.INPUT_METHOD_SERVICE);
final IBinder token = ims.getWindow().getWindow().getAttributes().token;
//imm.setInputMethod(token, LATIN);
imm.switchToLastInputMethod(token);
} catch (Throwable t) { // java.lang.NoSuchMethodError if API_level<11
Log.e("KP2A","cannot set the previous input method:");
t.printStackTrace();
SharedPreferences prefs = ims.getSharedPreferences(KP2A_SWITCHER, Context.MODE_PRIVATE);
switchToKeyboard(ims, prefs.getString(PREVIOUS_KEYBOARD, null), silent);
}
}
//silent: if true, do not show picker, only switch in background. Don't do anything if switching fails.
public static void switchToKeyboard(Context ctx, String newImeName, boolean silent)
{
Log.d(Tag,"silent: "+silent);
if ((newImeName == null) || (!autoSwitchEnabled(ctx)))
{
Log.d(Tag, "(newImeName == null): "+(newImeName == null));
Log.d(Tag, "autoSwitchEnabled(ctx)"+autoSwitchEnabled(ctx));
if (!silent)
{
showPicker(ctx);
}
return;
}
Intent qi = new Intent("com.twofortyfouram.locale.intent.action.FIRE_SETTING");
List<ResolveInfo> pkgAppsList = ctx.getPackageManager().queryBroadcastReceivers(qi, 0);
boolean sentBroadcast = false;
for (ResolveInfo ri: pkgAppsList)
{
if (ri.activityInfo.packageName.equals(SECURE_SETTINGS_PACKAGE_NAME))
{
String currentIme = android.provider.Settings.Secure.getString(
ctx.getContentResolver(),
android.provider.Settings.Secure.DEFAULT_INPUT_METHOD);
currentIme += ";"+String.valueOf(
android.provider.Settings.Secure.getInt(
ctx.getContentResolver(),
android.provider.Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
-1)
);
SharedPreferences prefs = ctx.getSharedPreferences(KP2A_SWITCHER, Context.MODE_PRIVATE);
Editor edit = prefs.edit();
edit.putString(PREVIOUS_KEYBOARD, currentIme);
edit.commit();
Intent i=new Intent("com.twofortyfouram.locale.intent.action.FIRE_SETTING");
Bundle b = new Bundle();
b.putString("com.intangibleobject.securesettings.plugin.extra.BLURB", "Input Method/SwitchIME");
b.putString("com.intangibleobject.securesettings.plugin.extra.INPUT_METHOD", newImeName);
b.putString("com.intangibleobject.securesettings.plugin.extra.SETTING","default_input_method");
i.putExtra("com.twofortyfouram.locale.intent.extra.BUNDLE", b);
i.setPackage(SECURE_SETTINGS_PACKAGE_NAME);
Log.d(Tag,"trying to switch by broadcast to SecureSettings");
ctx.sendBroadcast(i);
sentBroadcast = true;
break;
}
}
if ((!sentBroadcast) && (!silent))
{
showPicker(ctx);
}
}
private static boolean autoSwitchEnabled(Context ctx) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(ctx);
return sp.getBoolean("kp2a_switch_rooted", false);
}
private static void showPicker(Context ctx) {
((InputMethodManager) ctx.getSystemService(InputMethodService.INPUT_METHOD_SERVICE))
.showInputMethodPicker();
}
}

View File

@ -0,0 +1,24 @@
package keepass2android.kbbridge;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import android.text.TextUtils;
public class KeyboardData
{
public static List<StringForTyping> availableFields = new ArrayList<StringForTyping>();
public static String entryName;
public static String entryId;
public static boolean hasData()
{
return !TextUtils.isEmpty(entryId);
}
public static void clear()
{
availableFields.clear();
entryName = entryId = "";
}
}

View File

@ -0,0 +1,20 @@
package keepass2android.kbbridge;
import java.util.ArrayList;
import java.util.HashMap;
public class KeyboardDataBuilder {
private ArrayList<StringForTyping> availableFields = new ArrayList<StringForTyping>();
public void addString(String key, String displayName, String valueToType)
{
StringForTyping stringToType = new StringForTyping();
stringToType.key = key;
stringToType.displayName = displayName;
stringToType.value = valueToType;
availableFields.add(stringToType);
}
public void commit()
{
KeyboardData.availableFields = this.availableFields;
}
}

View File

@ -0,0 +1,20 @@
package keepass2android.kbbridge;
public class StringForTyping {
public String key; //internal identifier (PwEntry string field key)
public String displayName; //display name for displaying the key (might be translated)
public String value;
@Override
public StringForTyping clone(){
StringForTyping theClone = new StringForTyping();
theClone.key = key;
theClone.displayName = displayName;
theClone.value = value;
return theClone;
}
}

View File

@ -0,0 +1,259 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import java.util.HashMap;
import java.util.Set;
import java.util.Map.Entry;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.util.Log;
/**
* Stores new words temporarily until they are promoted to the user dictionary
* for longevity. Words in the auto dictionary are used to determine if it's ok
* to accept a word that's not in the main or user dictionary. Using a new word
* repeatedly will promote it to the user dictionary.
*/
public class AutoDictionary extends ExpandableDictionary {
// Weight added to a user picking a new word from the suggestion strip
static final int FREQUENCY_FOR_PICKED = 3;
// Weight added to a user typing a new word that doesn't get corrected (or is reverted)
static final int FREQUENCY_FOR_TYPED = 1;
// A word that is frequently typed and gets promoted to the user dictionary, uses this
// frequency.
static final int FREQUENCY_FOR_AUTO_ADD = 250;
// If the user touches a typed word 2 times or more, it will become valid.
private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
// If the user touches a typed word 4 times or more, it will be added to the user dict.
private static final int PROMOTION_THRESHOLD = 4 * FREQUENCY_FOR_PICKED;
private KP2AKeyboard mIme;
// Locale for which this auto dictionary is storing words
private String mLocale;
private HashMap<String,Integer> mPendingWrites = new HashMap<String,Integer>();
private final Object mPendingWritesLock = new Object();
private static final String DATABASE_NAME = "auto_dict.db";
private static final int DATABASE_VERSION = 1;
// These are the columns in the dictionary
// TODO: Consume less space by using a unique id for locale instead of the whole
// 2-5 character string.
private static final String COLUMN_ID = BaseColumns._ID;
private static final String COLUMN_WORD = "word";
private static final String COLUMN_FREQUENCY = "freq";
private static final String COLUMN_LOCALE = "locale";
/** Sort by descending order of frequency. */
public static final String DEFAULT_SORT_ORDER = COLUMN_FREQUENCY + " DESC";
/** Name of the words table in the auto_dict.db */
private static final String AUTODICT_TABLE_NAME = "words";
private static HashMap<String, String> sDictProjectionMap;
static {
sDictProjectionMap = new HashMap<String, String>();
sDictProjectionMap.put(COLUMN_ID, COLUMN_ID);
sDictProjectionMap.put(COLUMN_WORD, COLUMN_WORD);
sDictProjectionMap.put(COLUMN_FREQUENCY, COLUMN_FREQUENCY);
sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE);
}
private static DatabaseHelper sOpenHelper = null;
public AutoDictionary(Context context, KP2AKeyboard ime, String locale, int dicTypeId) {
super(context, dicTypeId);
mIme = ime;
mLocale = locale;
if (sOpenHelper == null) {
sOpenHelper = new DatabaseHelper(getContext());
}
if (mLocale != null && mLocale.length() > 1) {
loadDictionary();
}
}
@Override
public boolean isValidWord(CharSequence word) {
final int frequency = getWordFrequency(word);
return frequency >= VALIDITY_THRESHOLD;
}
@Override
public void close() {
flushPendingWrites();
// Don't close the database as locale changes will require it to be reopened anyway
// Also, the database is written to somewhat frequently, so it needs to be kept alive
// throughout the life of the process.
// mOpenHelper.close();
super.close();
}
@Override
public void loadDictionaryAsync() {
// Load the words that correspond to the current input locale
Cursor cursor = query(COLUMN_LOCALE + "=?", new String[] { mLocale });
try {
if (cursor.moveToFirst()) {
int wordIndex = cursor.getColumnIndex(COLUMN_WORD);
int frequencyIndex = cursor.getColumnIndex(COLUMN_FREQUENCY);
while (!cursor.isAfterLast()) {
String word = cursor.getString(wordIndex);
int frequency = cursor.getInt(frequencyIndex);
// Safeguard against adding really long words. Stack may overflow due
// to recursive lookup
if (word.length() < getMaxWordLength()) {
super.addWord(word, frequency);
}
cursor.moveToNext();
}
}
} finally {
cursor.close();
}
}
@Override
public void addWord(String word, int addFrequency) {
final int length = word.length();
// Don't add very short or very long words.
if (length < 2 || length > getMaxWordLength()) return;
if (mIme.getCurrentWord().isAutoCapitalized()) {
// Remove caps before adding
word = Character.toLowerCase(word.charAt(0)) + word.substring(1);
}
int freq = getWordFrequency(word);
freq = freq < 0 ? addFrequency : freq + addFrequency;
super.addWord(word, freq);
if (freq >= PROMOTION_THRESHOLD) {
mIme.promoteToUserDictionary(word, FREQUENCY_FOR_AUTO_ADD);
freq = 0;
}
synchronized (mPendingWritesLock) {
// Write a null frequency if it is to be deleted from the db
mPendingWrites.put(word, freq == 0 ? null : new Integer(freq));
}
}
/**
* Schedules a background thread to write any pending words to the database.
*/
public void flushPendingWrites() {
synchronized (mPendingWritesLock) {
// Nothing pending? Return
if (mPendingWrites.isEmpty()) return;
// Create a background thread to write the pending entries
new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute();
// Create a new map for writing new entries into while the old one is written to db
mPendingWrites = new HashMap<String, Integer>();
}
}
/**
* This class helps open, create, and upgrade the database file.
*/
private static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + AUTODICT_TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_WORD + " TEXT,"
+ COLUMN_FREQUENCY + " INTEGER,"
+ COLUMN_LOCALE + " TEXT"
+ ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w("AutoDictionary", "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + AUTODICT_TABLE_NAME);
onCreate(db);
}
}
private Cursor query(String selection, String[] selectionArgs) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(AUTODICT_TABLE_NAME);
qb.setProjectionMap(sDictProjectionMap);
// Get the database and run the query
SQLiteDatabase db = sOpenHelper.getReadableDatabase();
Cursor c = qb.query(db, null, selection, selectionArgs, null, null,
DEFAULT_SORT_ORDER);
return c;
}
/**
* Async task to write pending words to the database so that it stays in sync with
* the in-memory trie.
*/
private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
private final HashMap<String, Integer> mMap;
private final DatabaseHelper mDbHelper;
private final String mLocale;
public UpdateDbTask(Context context, DatabaseHelper openHelper,
HashMap<String, Integer> pendingWrites, String locale) {
mMap = pendingWrites;
mLocale = locale;
mDbHelper = openHelper;
}
@Override
protected Void doInBackground(Void... v) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
// Write all the entries to the db
Set<Entry<String,Integer>> mEntries = mMap.entrySet();
for (Entry<String,Integer> entry : mEntries) {
Integer freq = entry.getValue();
db.delete(AUTODICT_TABLE_NAME, COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
new String[] { entry.getKey(), mLocale });
if (freq != null) {
db.insert(AUTODICT_TABLE_NAME, null,
getContentValues(entry.getKey(), freq, mLocale));
}
}
return null;
}
private ContentValues getContentValues(String word, int frequency, String locale) {
ContentValues values = new ContentValues(4);
values.put(COLUMN_WORD, word);
values.put(COLUMN_FREQUENCY, frequency);
values.put(COLUMN_LOCALE, locale);
return values;
}
}
}

View File

@ -0,0 +1,300 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import java.io.InputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.util.Arrays;
import android.content.Context;
import android.util.Log;
/**
* Implements a static, compacted, binary dictionary of standard words.
*/
public class BinaryDictionary extends Dictionary {
/**
* There is difference between what java and native code can handle.
* This value should only be used in BinaryDictionary.java
* It is necessary to keep it at this value because some languages e.g. German have
* really long words.
*/
protected static final int MAX_WORD_LENGTH = 48;
private static final String TAG = "BinaryDictionary";
private static final int MAX_ALTERNATIVES = 16;
private static final int MAX_WORDS = 18;
private static final int MAX_BIGRAMS = 60;
private static final int TYPED_LETTER_MULTIPLIER = 2;
private static final boolean ENABLE_MISSED_CHARACTERS = true;
private int mDicTypeId;
private int mNativeDict;
private int mDictLength;
private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES];
private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
private char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
private int[] mFrequencies = new int[MAX_WORDS];
private int[] mFrequencies_bigrams = new int[MAX_BIGRAMS];
// Keep a reference to the native dict direct buffer in Java to avoid
// unexpected deallocation of the direct buffer.
private ByteBuffer mNativeDictDirectBuffer;
static {
try {
System.loadLibrary("jni_latinime");
} catch (UnsatisfiedLinkError ule) {
Log.e("BinaryDictionary", "Could not load native library jni_latinime");
}
}
/**
* Create a dictionary from a raw resource file
* @param context application context for reading resources
* @param resId the resource containing the raw binary dictionary
*/
public BinaryDictionary(Context context, int[] resId, int dicTypeId) {
if (resId != null && resId.length > 0 && resId[0] != 0) {
loadDictionary(context, resId);
}
mDicTypeId = dicTypeId;
}
/**
* Create a dictionary from input streams
* @param context application context for reading resources
* @param streams the resource streams containing the raw binary dictionary
*/
public BinaryDictionary(Context context, InputStream[] streams, int dicTypeId) {
if (streams != null && streams.length > 0) {
loadDictionary(context, streams);
}
mDicTypeId = dicTypeId;
}
/**
* Create a dictionary from a byte buffer. This is used for testing.
* @param context application context for reading resources
* @param byteBuffer a ByteBuffer containing the binary dictionary
*/
public BinaryDictionary(Context context, ByteBuffer byteBuffer, int dicTypeId) {
if (byteBuffer != null) {
if (byteBuffer.isDirect()) {
mNativeDictDirectBuffer = byteBuffer;
} else {
mNativeDictDirectBuffer = ByteBuffer.allocateDirect(byteBuffer.capacity());
byteBuffer.rewind();
mNativeDictDirectBuffer.put(byteBuffer);
}
mDictLength = byteBuffer.capacity();
mNativeDict = openNative(mNativeDictDirectBuffer,
TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER);
}
mDicTypeId = dicTypeId;
}
private native int openNative(ByteBuffer bb, int typedLetterMultiplier,
int fullWordMultiplier);
private native void closeNative(int dict);
private native boolean isValidWordNative(int nativeData, char[] word, int wordLength);
private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize,
char[] outputChars, int[] frequencies, int maxWordLength, int maxWords,
int maxAlternatives, int skipPos, int[] nextLettersFrequencies, int nextLettersSize);
private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength,
int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies,
int maxWordLength, int maxBigrams, int maxAlternatives);
private final void loadDictionary(Context context, int[] resId) {
InputStream[] is = null;
try {
// merging separated dictionary into one if dictionary is separated
is = new InputStream[resId.length];
for (int i = 0; i < resId.length; i++) {
is[i] = context.getResources().openRawResource(resId[i]);
}
loadDictionary(context, is);
} finally {
try {
if (is != null) {
for (int i = 0; i < is.length; i++) {
is[i].close();
}
}
} catch (IOException e) {
Log.w(TAG, "Failed to close input stream");
}
}
}
private void loadDictionary(Context context, InputStream[] is)
{
try
{
int total = 0;
for (int i = 0; i < is.length; i++)
total += is[i].available();
mNativeDictDirectBuffer =
ByteBuffer.allocateDirect(total).order(ByteOrder.nativeOrder());
int got = 0;
for (int i = 0; i < is.length; i++) {
got += Channels.newChannel(is[i]).read(mNativeDictDirectBuffer);
}
if (got != total) {
Log.e(TAG, "Read " + got + " bytes, expected " + total);
} else {
mNativeDict = openNative(mNativeDictDirectBuffer,
TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER);
mDictLength = total;
}
}
catch (IOException e) {
Log.w(TAG, "No available memory for binary dictionary");
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, "Failed to load native dictionary", e);
} finally {
try {
if (is != null) {
for (int i = 0; i < is.length; i++) {
is[i].close();
}
}
} catch (IOException e) {
Log.w(TAG, "Failed to close input stream");
}
}
}
@Override
public void getBigrams(final WordComposer codes, final CharSequence previousWord,
final WordCallback callback, int[] nextLettersFrequencies) {
char[] chars = previousWord.toString().toCharArray();
Arrays.fill(mOutputChars_bigrams, (char) 0);
Arrays.fill(mFrequencies_bigrams, 0);
int codesSize = codes.size();
Arrays.fill(mInputCodes, -1);
int[] alternatives = codes.getCodesAt(0);
System.arraycopy(alternatives, 0, mInputCodes, 0,
Math.min(alternatives.length, MAX_ALTERNATIVES));
int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize,
mOutputChars_bigrams, mFrequencies_bigrams, MAX_WORD_LENGTH, MAX_BIGRAMS,
MAX_ALTERNATIVES);
for (int j = 0; j < count; j++) {
if (mFrequencies_bigrams[j] < 1) break;
int start = j * MAX_WORD_LENGTH;
int len = 0;
while (mOutputChars_bigrams[start + len] != 0) {
len++;
}
if (len > 0) {
callback.addWord(mOutputChars_bigrams, start, len, mFrequencies_bigrams[j],
mDicTypeId, DataType.BIGRAM);
}
}
}
@Override
public void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
final int codesSize = codes.size();
// Won't deal with really long words.
if (codesSize > MAX_WORD_LENGTH - 1) return;
Arrays.fill(mInputCodes, -1);
for (int i = 0; i < codesSize; i++) {
int[] alternatives = codes.getCodesAt(i);
System.arraycopy(alternatives, 0, mInputCodes, i * MAX_ALTERNATIVES,
Math.min(alternatives.length, MAX_ALTERNATIVES));
}
Arrays.fill(mOutputChars, (char) 0);
Arrays.fill(mFrequencies, 0);
int count = getSuggestionsNative(mNativeDict, mInputCodes, codesSize,
mOutputChars, mFrequencies,
MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, -1,
nextLettersFrequencies,
nextLettersFrequencies != null ? nextLettersFrequencies.length : 0);
// If there aren't sufficient suggestions, search for words by allowing wild cards at
// the different character positions. This feature is not ready for prime-time as we need
// to figure out the best ranking for such words compared to proximity corrections and
// completions.
if (ENABLE_MISSED_CHARACTERS && count < 5) {
for (int skip = 0; skip < codesSize; skip++) {
int tempCount = getSuggestionsNative(mNativeDict, mInputCodes, codesSize,
mOutputChars, mFrequencies,
MAX_WORD_LENGTH, MAX_WORDS, MAX_ALTERNATIVES, skip,
null, 0);
count = Math.max(count, tempCount);
if (tempCount > 0) break;
}
}
for (int j = 0; j < count; j++) {
if (mFrequencies[j] < 1) break;
int start = j * MAX_WORD_LENGTH;
int len = 0;
while (mOutputChars[start + len] != 0) {
len++;
}
if (len > 0) {
callback.addWord(mOutputChars, start, len, mFrequencies[j], mDicTypeId,
DataType.UNIGRAM);
}
}
}
@Override
public boolean isValidWord(CharSequence word) {
if (word == null) return false;
char[] chars = word.toString().toCharArray();
return isValidWordNative(mNativeDict, chars, chars.length);
}
public int getSize() {
return mDictLength; // This value is initialized on the call to openNative()
}
@Override
public synchronized void close() {
if (mNativeDict != 0) {
closeNative(mNativeDict);
mNativeDict = 0;
}
}
@Override
protected void finalize() throws Throwable {
close();
super.finalize();
}
}

View File

@ -0,0 +1,491 @@
/*
* Copyright (C) 2008 The Android Open Source Project
* Copyright (C) 2014 Philipp Crocoll <crocoapps@googlemail.com>
* Copyright (C) 2014 Wiktor Lawski <wiktor.lawski@gmail.com>
*
* 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 keepass2android.softkeyboard;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.PopupWindow;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CandidateView extends View {
private static final int OUT_OF_BOUNDS_WORD_INDEX = -1;
private static final int OUT_OF_BOUNDS_X_COORD = -1;
private KP2AKeyboard mService;
private final ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
private boolean mShowingCompletions;
private CharSequence mSelectedString;
private int mSelectedIndex;
private int mTouchX = OUT_OF_BOUNDS_X_COORD;
private final Drawable mSelectionHighlight;
private boolean mTypedWordValid;
private boolean mHaveMinimalSuggestion;
private Rect mBgPadding;
private final TextView mPreviewText;
private final PopupWindow mPreviewPopup;
private int mCurrentWordIndex;
private Drawable mDivider;
private static final int MAX_SUGGESTIONS = 32;
private static final int SCROLL_PIXELS = 20;
private final int[] mWordWidth = new int[MAX_SUGGESTIONS];
private final int[] mWordX = new int[MAX_SUGGESTIONS];
private int mPopupPreviewX;
private int mPopupPreviewY;
private static final int X_GAP = 10;
private final int mColorNormal;
private final int mColorRecommended;
private final int mColorOther;
private final Paint mPaint;
private final int mDescent;
private boolean mScrolled;
private boolean mShowingAddToDictionary;
private CharSequence mAddToDictionaryHint;
private int mTargetScrollX;
private final int mMinTouchableWidth;
private int mTotalWidth;
private final GestureDetector mGestureDetector;
/**
* Construct a CandidateView for showing suggested words for completion.
* @param context
* @param attrs
*/
public CandidateView(Context context, AttributeSet attrs) {
super(context, attrs);
mSelectionHighlight = context.getResources().getDrawable(
R.drawable.list_selector_background_pressed);
LayoutInflater inflate =
(LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Resources res = context.getResources();
mPreviewPopup = new PopupWindow(context);
mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null);
mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
mPreviewPopup.setContentView(mPreviewText);
mPreviewPopup.setBackgroundDrawable(null);
mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
mColorNormal = res.getColor(R.color.candidate_normal);
mColorRecommended = res.getColor(R.color.candidate_recommended);
mColorOther = res.getColor(R.color.candidate_other);
mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider);
mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary);
mPaint = new Paint();
mPaint.setColor(mColorNormal);
mPaint.setAntiAlias(true);
mPaint.setTextSize(mPreviewText.getTextSize());
mPaint.setStrokeWidth(0);
mPaint.setTextAlign(Align.CENTER);
mDescent = (int) mPaint.descent();
mMinTouchableWidth = (int)res.getDimension(R.dimen.candidate_min_touchable_width);
mGestureDetector = new GestureDetector(
new CandidateStripGestureListener(mMinTouchableWidth));
setWillNotDraw(false);
setHorizontalScrollBarEnabled(false);
setVerticalScrollBarEnabled(false);
scrollTo(0, getScrollY());
}
private class CandidateStripGestureListener extends GestureDetector.SimpleOnGestureListener {
private final int mTouchSlopSquare;
public CandidateStripGestureListener(int touchSlop) {
// Slightly reluctant to scroll to be able to easily choose the suggestion
mTouchSlopSquare = touchSlop * touchSlop;
}
@Override
public void onLongPress(MotionEvent me) {
if (mSuggestions.size() > 0) {
if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) {
longPressFirstWord();
}
}
}
@Override
public boolean onDown(MotionEvent e) {
mScrolled = false;
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
if (!mScrolled) {
// This is applied only when we recognize that scrolling is starting.
final int deltaX = (int) (e2.getX() - e1.getX());
final int deltaY = (int) (e2.getY() - e1.getY());
final int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance < mTouchSlopSquare) {
return true;
}
mScrolled = true;
}
final int width = getWidth();
mScrolled = true;
int scrollX = getScrollX();
scrollX += (int) distanceX;
if (scrollX < 0) {
scrollX = 0;
}
if (distanceX > 0 && scrollX + width > mTotalWidth) {
scrollX -= (int) distanceX;
}
mTargetScrollX = scrollX;
scrollTo(scrollX, getScrollY());
hidePreview();
invalidate();
return true;
}
}
/**
* A connection back to the service to communicate with the text field
* @param listener
*/
public void setService(KP2AKeyboard listener) {
mService = listener;
}
@Override
public int computeHorizontalScrollRange() {
return mTotalWidth;
}
/**
* If the canvas is null, then only touch calculations are performed to pick the target
* candidate.
*/
@Override
protected void onDraw(Canvas canvas) {
if (canvas != null) {
super.onDraw(canvas);
}
mTotalWidth = 0;
final int height = getHeight();
if (mBgPadding == null) {
mBgPadding = new Rect(0, 0, 0, 0);
if (getBackground() != null) {
getBackground().getPadding(mBgPadding);
}
mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(),
mDivider.getIntrinsicHeight());
}
final int count = mSuggestions.size();
final Rect bgPadding = mBgPadding;
final Paint paint = mPaint;
final int touchX = mTouchX;
final int scrollX = getScrollX();
final boolean scrolled = mScrolled;
final boolean typedWordValid = mTypedWordValid;
final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2;
boolean existsAutoCompletion = false;
int x = 0;
for (int i = 0; i < count; i++) {
CharSequence suggestion = mSuggestions.get(i);
if (suggestion == null) continue;
final int wordLength = suggestion.length();
paint.setColor(mColorNormal);
if (mHaveMinimalSuggestion
&& ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) {
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setColor(mColorRecommended);
existsAutoCompletion = true;
} else if (i != 0 || (wordLength == 1 && count > 1)) {
// HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 and
// there are multiple suggestions, such as the default punctuation list.
paint.setColor(mColorOther);
}
int wordWidth;
if ((wordWidth = mWordWidth[i]) == 0) {
float textWidth = paint.measureText(suggestion, 0, wordLength);
wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2);
mWordWidth[i] = wordWidth;
}
mWordX[i] = x;
if (touchX != OUT_OF_BOUNDS_X_COORD && !scrolled
&& touchX + scrollX >= x && touchX + scrollX < x + wordWidth) {
if (canvas != null && !mShowingAddToDictionary) {
canvas.translate(x, 0);
mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height);
mSelectionHighlight.draw(canvas);
canvas.translate(-x, 0);
}
mSelectedString = suggestion;
mSelectedIndex = i;
}
if (canvas != null) {
canvas.drawText(suggestion, 0, wordLength, x + wordWidth / 2, y, paint);
paint.setColor(mColorOther);
canvas.translate(x + wordWidth, 0);
// Draw a divider unless it's after the hint
if (!(mShowingAddToDictionary && i == 1)) {
mDivider.draw(canvas);
}
canvas.translate(-x - wordWidth, 0);
}
paint.setTypeface(Typeface.DEFAULT);
x += wordWidth;
}
mService.onAutoCompletionStateChanged(existsAutoCompletion);
mTotalWidth = x;
if (mTargetScrollX != scrollX) {
scrollToTarget();
}
}
private void scrollToTarget() {
int scrollX = getScrollX();
if (mTargetScrollX > scrollX) {
scrollX += SCROLL_PIXELS;
if (scrollX >= mTargetScrollX) {
scrollX = mTargetScrollX;
scrollTo(scrollX, getScrollY());
requestLayout();
} else {
scrollTo(scrollX, getScrollY());
}
} else {
scrollX -= SCROLL_PIXELS;
if (scrollX <= mTargetScrollX) {
scrollX = mTargetScrollX;
scrollTo(scrollX, getScrollY());
requestLayout();
} else {
scrollTo(scrollX, getScrollY());
}
}
invalidate();
}
@SuppressLint("WrongCall")
public void setSuggestions(List<CharSequence> suggestions, boolean completions,
boolean typedWordValid, boolean haveMinimalSuggestion) {
clear();
if (suggestions != null) {
int insertCount = Math.min(suggestions.size(), MAX_SUGGESTIONS);
for (CharSequence suggestion : suggestions) {
mSuggestions.add(suggestion);
if (--insertCount == 0)
break;
}
}
mShowingCompletions = completions;
mTypedWordValid = typedWordValid;
scrollTo(0, getScrollY());
mTargetScrollX = 0;
mHaveMinimalSuggestion = haveMinimalSuggestion;
// Compute the total width
onDraw(null);
invalidate();
requestLayout();
}
public boolean isShowingAddToDictionaryHint() {
return mShowingAddToDictionary;
}
public void showAddToDictionaryHint(CharSequence word) {
ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>();
suggestions.add(word);
suggestions.add(mAddToDictionaryHint);
setSuggestions(suggestions, false, false, false);
mShowingAddToDictionary = true;
}
public boolean dismissAddToDictionaryHint() {
if (!mShowingAddToDictionary) return false;
clear();
return true;
}
/* package */ List<CharSequence> getSuggestions() {
return mSuggestions;
}
public void clear() {
// Don't call mSuggestions.clear() because it's being used for logging
// in LatinIME.pickSuggestionManually().
mSuggestions.clear();
mTouchX = OUT_OF_BOUNDS_X_COORD;
mSelectedString = null;
mSelectedIndex = -1;
mShowingAddToDictionary = false;
invalidate();
Arrays.fill(mWordWidth, 0);
Arrays.fill(mWordX, 0);
}
@Override
public boolean onTouchEvent(MotionEvent me) {
if (mGestureDetector.onTouchEvent(me)) {
return true;
}
int action = me.getAction();
int x = (int) me.getX();
int y = (int) me.getY();
mTouchX = x;
switch (action) {
case MotionEvent.ACTION_DOWN:
invalidate();
break;
case MotionEvent.ACTION_MOVE:
if (y <= 0) {
// Fling up!?
if (mSelectedString != null) {
// If there are completions from the application, we don't change the state to
// STATE_PICKED_SUGGESTION
if (!mShowingCompletions) {
// This "acceptedSuggestion" will not be counted as a word because
// it will be counted in pickSuggestion instead.
TextEntryState.acceptedSuggestion(mSuggestions.get(0),
mSelectedString);
}
mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
mSelectedString = null;
mSelectedIndex = -1;
}
}
break;
case MotionEvent.ACTION_UP:
if (!mScrolled) {
if (mSelectedString != null) {
if (mShowingAddToDictionary) {
longPressFirstWord();
clear();
} else {
if (!mShowingCompletions) {
TextEntryState.acceptedSuggestion(mSuggestions.get(0),
mSelectedString);
}
mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
}
}
}
mSelectedString = null;
mSelectedIndex = -1;
requestLayout();
hidePreview();
invalidate();
break;
}
return true;
}
private void hidePreview() {
mTouchX = OUT_OF_BOUNDS_X_COORD;
mCurrentWordIndex = OUT_OF_BOUNDS_WORD_INDEX;
mPreviewPopup.dismiss();
}
private void showPreview(int wordIndex, String altText) {
int oldWordIndex = mCurrentWordIndex;
mCurrentWordIndex = wordIndex;
// If index changed or changing text
if (oldWordIndex != mCurrentWordIndex || altText != null) {
if (wordIndex == OUT_OF_BOUNDS_WORD_INDEX) {
hidePreview();
} else {
CharSequence word = altText != null? altText : mSuggestions.get(wordIndex);
mPreviewText.setText(word);
mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2);
final int popupWidth = wordWidth
+ mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight();
final int popupHeight = mPreviewText.getMeasuredHeight();
//mPreviewText.setVisibility(INVISIBLE);
mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX()
+ (mWordWidth[wordIndex] - wordWidth) / 2;
mPopupPreviewY = - popupHeight;
int [] offsetInWindow = new int[2];
getLocationInWindow(offsetInWindow);
if (mPreviewPopup.isShowing()) {
mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1],
popupWidth, popupHeight);
} else {
mPreviewPopup.setWidth(popupWidth);
mPreviewPopup.setHeight(popupHeight);
mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX,
mPopupPreviewY + offsetInWindow[1]);
}
mPreviewText.setVisibility(VISIBLE);
}
}
}
private void longPressFirstWord() {
CharSequence word = mSuggestions.get(0);
if (word.length() < 2) return;
if (mService.addWordToDictionary(word.toString())) {
showPreview(0, getContext().getResources().getString(R.string.added_word, word));
}
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
hidePreview();
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package keepass2android.softkeyboard;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.SystemClock;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Log;
public class ContactsDictionary extends ExpandableDictionary {
private static final String[] PROJECTION = {
Contacts._ID,
Contacts.DISPLAY_NAME,
};
private static final String TAG = "ContactsDictionary";
/**
* Frequency for contacts information into the dictionary
*/
private static final int FREQUENCY_FOR_CONTACTS = 128;
private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
private static final int INDEX_NAME = 1;
private ContentObserver mObserver;
private long mLastLoadedContacts;
public ContactsDictionary(Context context, int dicTypeId) {
super(context, dicTypeId);
// Perform a managed query. The Activity will handle closing and requerying the cursor
// when needed.
ContentResolver cres = context.getContentResolver();
cres.registerContentObserver(
Contacts.CONTENT_URI, true,mObserver = new ContentObserver(null) {
@Override
public void onChange(boolean self) {
setRequiresReload(true);
}
});
loadDictionary();
}
@Override
public synchronized void close() {
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
mObserver = null;
}
super.close();
}
@Override
public void startDictionaryLoadingTaskLocked() {
long now = SystemClock.uptimeMillis();
if (mLastLoadedContacts == 0
|| now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
super.startDictionaryLoadingTaskLocked();
}
}
@Override
public void loadDictionaryAsync() {
/*try {
Cursor cursor = getContext().getContentResolver()
.query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
if (cursor != null) {
addWords(cursor);
}
} catch(IllegalStateException e) {
Log.e(TAG, "Contacts DB is having problems");
}
mLastLoadedContacts = SystemClock.uptimeMillis();*/
}
private void addWords(Cursor cursor) {
clearDictionary();
final int maxWordLength = getMaxWordLength();
try {
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
String name = cursor.getString(INDEX_NAME);
if (name != null) {
int len = name.length();
String prevWord = null;
// TODO: Better tokenization for non-Latin writing systems
for (int i = 0; i < len; i++) {
if (Character.isLetter(name.charAt(i))) {
int j;
for (j = i + 1; j < len; j++) {
char c = name.charAt(j);
if (!(c == '-' || c == '\'' ||
Character.isLetter(c))) {
break;
}
}
String word = name.substring(i, j);
i = j - 1;
// Safeguard against adding really long words. Stack
// may overflow due to recursion
// Also don't add single letter words, possibly confuses
// capitalization of i.
final int wordLen = word.length();
if (wordLen < maxWordLength && wordLen > 1) {
super.addWord(word, FREQUENCY_FOR_CONTACTS);
if (!TextUtils.isEmpty(prevWord)) {
// TODO Do not add email address
// Not so critical
super.setBigram(prevWord, word,
FREQUENCY_FOR_CONTACTS_BIGRAM);
}
prevWord = word;
}
}
}
}
cursor.moveToNext();
}
}
cursor.close();
} catch(IllegalStateException e) {
Log.e(TAG, "Contacts DB is having problems");
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2014 Wiktor Lawski <wiktor.lawski@gmail.com>
*
* 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 keepass2android.softkeyboard;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.SharedPreferences;
public class Design {
@SuppressLint("InlinedApi")
public static void updateTheme(Activity activity, SharedPreferences prefs) {
if (android.os.Build.VERSION.SDK_INT >= 11
/* android.os.Build.VERSION_CODES.HONEYCOMB */) {
String design = prefs.getString("design_key", "Light");
if (design.equals("Light")) {
activity.setTheme(android.R.style.Theme_Holo_Light);
} else {
activity.setTheme(android.R.style.Theme_Holo);
}
}
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
/**
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
* strokes.
*/
abstract public class Dictionary {
/**
* Whether or not to replicate the typed word in the suggested list, even if it's valid.
*/
protected static final boolean INCLUDE_TYPED_WORD_IF_VALID = false;
/**
* The weight to give to a word if it's length is the same as the number of typed characters.
*/
protected static final int FULL_WORD_FREQ_MULTIPLIER = 2;
public static enum DataType {
UNIGRAM, BIGRAM
}
/**
* Interface to be implemented by classes requesting words to be fetched from the dictionary.
* @see #getWords(WordComposer, WordCallback)
*/
public interface WordCallback {
/**
* Adds a word to a list of suggestions. The word is expected to be ordered based on
* the provided frequency.
* @param word the character array containing the word
* @param wordOffset starting offset of the word in the character array
* @param wordLength length of valid characters in the character array
* @param frequency the frequency of occurence. This is normalized between 1 and 255, but
* can exceed those limits
* @param dicTypeId of the dictionary where word was from
* @param dataType tells type of this data
* @return true if the word was added, false if no more words are required
*/
boolean addWord(char[] word, int wordOffset, int wordLength, int frequency, int dicTypeId,
DataType dataType);
}
/**
* Searches for words in the dictionary that match the characters in the composer. Matched
* words are added through the callback object.
* @param composer the key sequence to match
* @param callback the callback object to send matched words to as possible candidates
* @param nextLettersFrequencies array of frequencies of next letters that could follow the
* word so far. For instance, "bracke" can be followed by "t", so array['t'] will have
* a non-zero value on returning from this method.
* Pass in null if you don't want the dictionary to look up next letters.
* @see WordCallback#addWord(char[], int, int)
*/
abstract public void getWords(final WordComposer composer, final WordCallback callback,
int[] nextLettersFrequencies);
/**
* Searches for pairs in the bigram dictionary that matches the previous word and all the
* possible words following are added through the callback object.
* @param composer the key sequence to match
* @param callback the callback object to send possible word following previous word
* @param nextLettersFrequencies array of frequencies of next letters that could follow the
* word so far. For instance, "bracke" can be followed by "t", so array['t'] will have
* a non-zero value on returning from this method.
* Pass in null if you don't want the dictionary to look up next letters.
*/
public void getBigrams(final WordComposer composer, final CharSequence previousWord,
final WordCallback callback, int[] nextLettersFrequencies) {
// empty base implementation
}
/**
* Checks if the given word occurs in the dictionary
* @param word the word to search for. The search should be case-insensitive.
* @return true if the word exists, false otherwise
*/
abstract public boolean isValidWord(CharSequence word);
/**
* Compares the contents of the character array with the typed word and returns true if they
* are the same.
* @param word the array of characters that make up the word
* @param length the number of valid characters in the character array
* @param typedWord the word to compare with
* @return true if they are the same, false otherwise.
*/
protected boolean same(final char[] word, final int length, final CharSequence typedWord) {
if (typedWord.length() != length) {
return false;
}
for (int i = 0; i < length; i++) {
if (word[i] != typedWord.charAt(i)) {
return false;
}
}
return true;
}
/**
* Override to clean up any resources.
*/
public void close() {
}
}

View File

@ -0,0 +1,337 @@
/*
* Copyright (C) 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.text.TextUtils;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* Utility methods to deal with editing text through an InputConnection.
*/
public class EditingUtil {
/**
* Number of characters we want to look back in order to identify the previous word
*/
private static final int LOOKBACK_CHARACTER_NUM = 15;
// Cache Method pointers
private static boolean sMethodsInitialized;
private static Method sMethodGetSelectedText;
private static Method sMethodSetComposingRegion;
private EditingUtil() {};
/**
* Append newText to the text field represented by connection.
* The new text becomes selected.
*/
public static void appendText(InputConnection connection, String newText) {
if (connection == null) {
return;
}
// Commit the composing text
connection.finishComposingText();
// Add a space if the field already has text.
CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
if (charBeforeCursor != null
&& !charBeforeCursor.equals(" ")
&& (charBeforeCursor.length() > 0)) {
newText = " " + newText;
}
connection.setComposingText(newText, 1);
}
private static int getCursorPosition(InputConnection connection) {
ExtractedText extracted = connection.getExtractedText(
new ExtractedTextRequest(), 0);
if (extracted == null) {
return -1;
}
return extracted.startOffset + extracted.selectionStart;
}
/**
* @param connection connection to the current text field.
* @param sep characters which may separate words
* @param range the range object to store the result into
* @return the word that surrounds the cursor, including up to one trailing
* separator. For example, if the field contains "he|llo world", where |
* represents the cursor, then "hello " will be returned.
*/
public static String getWordAtCursor(
InputConnection connection, String separators, Range range) {
Range r = getWordRangeAtCursor(connection, separators, range);
return (r == null) ? null : r.word;
}
/**
* Removes the word surrounding the cursor. Parameters are identical to
* getWordAtCursor.
*/
public static void deleteWordAtCursor(
InputConnection connection, String separators) {
Range range = getWordRangeAtCursor(connection, separators, null);
if (range == null) return;
connection.finishComposingText();
// Move cursor to beginning of word, to avoid crash when cursor is outside
// of valid range after deleting text.
int newCursor = getCursorPosition(connection) - range.charsBefore;
connection.setSelection(newCursor, newCursor);
connection.deleteSurroundingText(0, range.charsBefore + range.charsAfter);
}
/**
* Represents a range of text, relative to the current cursor position.
*/
public static class Range {
/** Characters before selection start */
public int charsBefore;
/**
* Characters after selection start, including one trailing word
* separator.
*/
public int charsAfter;
/** The actual characters that make up a word */
public String word;
public Range() {}
public Range(int charsBefore, int charsAfter, String word) {
if (charsBefore < 0 || charsAfter < 0) {
throw new IndexOutOfBoundsException();
}
this.charsBefore = charsBefore;
this.charsAfter = charsAfter;
this.word = word;
}
}
private static Range getWordRangeAtCursor(
InputConnection connection, String sep, Range range) {
if (connection == null || sep == null) {
return null;
}
CharSequence before = connection.getTextBeforeCursor(1000, 0);
CharSequence after = connection.getTextAfterCursor(1000, 0);
if (before == null || after == null) {
return null;
}
// Find first word separator before the cursor
int start = before.length();
while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
// Find last word separator after the cursor
int end = -1;
while (++end < after.length() && !isWhitespace(after.charAt(end), sep));
int cursor = getCursorPosition(connection);
if (start >= 0 && cursor + end <= after.length() + before.length()) {
String word = before.toString().substring(start, before.length())
+ after.toString().substring(0, end);
Range returnRange = range != null? range : new Range();
returnRange.charsBefore = before.length() - start;
returnRange.charsAfter = end;
returnRange.word = word;
return returnRange;
}
return null;
}
private static boolean isWhitespace(int code, String whitespace) {
return whitespace.contains(String.valueOf((char) code));
}
private static final Pattern spaceRegex = Pattern.compile("\\s+");
public static CharSequence getPreviousWord(InputConnection connection,
String sentenceSeperators) {
//TODO: Should fix this. This could be slow!
CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
if (prev == null) {
return null;
}
String[] w = spaceRegex.split(prev);
if (w.length >= 2 && w[w.length-2].length() > 0) {
char lastChar = w[w.length-2].charAt(w[w.length-2].length() -1);
if (sentenceSeperators.contains(String.valueOf(lastChar))) {
return null;
}
return w[w.length-2];
} else {
return null;
}
}
public static class SelectedWord {
public int start;
public int end;
public CharSequence word;
}
/**
* Takes a character sequence with a single character and checks if the character occurs
* in a list of word separators or is empty.
* @param singleChar A CharSequence with null, zero or one character
* @param wordSeparators A String containing the word separators
* @return true if the character is at a word boundary, false otherwise
*/
private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
}
/**
* Checks if the cursor is inside a word or the current selection is a whole word.
* @param ic the InputConnection for accessing the text field
* @param selStart the start position of the selection within the text field
* @param selEnd the end position of the selection within the text field. This could be
* the same as selStart, if there's no selection.
* @param wordSeparators the word separator characters for the current language
* @return an object containing the text and coordinates of the selected/touching word,
* null if the selection/cursor is not marking a whole word.
*/
public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
int selStart, int selEnd, String wordSeparators) {
if (selStart == selEnd) {
// There is just a cursor, so get the word at the cursor
EditingUtil.Range range = new EditingUtil.Range();
CharSequence touching = getWordAtCursor(ic, wordSeparators, range);
if (!TextUtils.isEmpty(touching)) {
SelectedWord selWord = new SelectedWord();
selWord.word = touching;
selWord.start = selStart - range.charsBefore;
selWord.end = selEnd + range.charsAfter;
return selWord;
}
} else {
// Is the previous character empty or a word separator? If not, return null.
CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
if (!isWordBoundary(charsBefore, wordSeparators)) {
return null;
}
// Is the next character empty or a word separator? If not, return null.
CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
if (!isWordBoundary(charsAfter, wordSeparators)) {
return null;
}
// Extract the selection alone
CharSequence touching = getSelectedText(ic, selStart, selEnd);
if (TextUtils.isEmpty(touching)) return null;
// Is any part of the selection a separator? If so, return null.
final int length = touching.length();
for (int i = 0; i < length; i++) {
if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
return null;
}
}
// Prepare the selected word
SelectedWord selWord = new SelectedWord();
selWord.start = selStart;
selWord.end = selEnd;
selWord.word = touching;
return selWord;
}
return null;
}
/**
* Cache method pointers for performance
*/
private static void initializeMethodsForReflection() {
try {
// These will either both exist or not, so no need for separate try/catch blocks.
// If other methods are added later, use separate try/catch blocks.
sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class);
sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion",
int.class, int.class);
} catch (NoSuchMethodException exc) {
// Ignore
}
sMethodsInitialized = true;
}
/**
* Returns the selected text between the selStart and selEnd positions.
*/
private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
// Use reflection, for backward compatibility
CharSequence result = null;
if (!sMethodsInitialized) {
initializeMethodsForReflection();
}
if (sMethodGetSelectedText != null) {
try {
result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0);
return result;
} catch (InvocationTargetException exc) {
// Ignore
} catch (IllegalArgumentException e) {
// Ignore
} catch (IllegalAccessException e) {
// Ignore
}
}
// Reflection didn't work, try it the poor way, by moving the cursor to the start,
// getting the text after the cursor and moving the text back to selected mode.
// TODO: Verify that this works properly in conjunction with
// LatinIME#onUpdateSelection
ic.setSelection(selStart, selEnd);
result = ic.getTextAfterCursor(selEnd - selStart, 0);
ic.setSelection(selStart, selEnd);
return result;
}
/**
* Tries to set the text into composition mode if there is support for it in the framework.
*/
public static void underlineWord(InputConnection ic, SelectedWord word) {
// Use reflection, for backward compatibility
// If method not found, there's nothing we can do. It still works but just wont underline
// the word.
if (!sMethodsInitialized) {
initializeMethodsForReflection();
}
if (sMethodSetComposingRegion != null) {
try {
sMethodSetComposingRegion.invoke(ic, word.start, word.end);
} catch (InvocationTargetException exc) {
// Ignore
} catch (IllegalArgumentException e) {
// Ignore
} catch (IllegalAccessException e) {
// Ignore
}
}
}
}

View File

@ -0,0 +1,691 @@
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package keepass2android.softkeyboard;
import java.util.LinkedList;
import android.content.Context;
import android.os.AsyncTask;
/**
* Base class for an in-memory dictionary that can grow dynamically and can
* be searched for suggestions and valid words.
*/
public class ExpandableDictionary extends Dictionary {
/**
* There is difference between what java and native code can handle.
* It uses 32 because Java stack overflows when greater value is used.
*/
protected static final int MAX_WORD_LENGTH = 32;
private Context mContext;
private char[] mWordBuilder = new char[MAX_WORD_LENGTH];
private int mDicTypeId;
private int mMaxDepth;
private int mInputLength;
private int[] mNextLettersFrequencies;
private StringBuilder sb = new StringBuilder(MAX_WORD_LENGTH);
private static final char QUOTE = '\'';
private boolean mRequiresReload;
private boolean mUpdatingDictionary;
// Use this lock before touching mUpdatingDictionary & mRequiresDownload
private Object mUpdatingLock = new Object();
static class Node {
char code;
int frequency;
boolean terminal;
Node parent;
NodeArray children;
LinkedList<NextWord> ngrams; // Supports ngram
}
static class NodeArray {
Node[] data;
int length = 0;
private static final int INCREMENT = 2;
NodeArray() {
data = new Node[INCREMENT];
}
void add(Node n) {
if (length + 1 > data.length) {
Node[] tempData = new Node[length + INCREMENT];
if (length > 0) {
System.arraycopy(data, 0, tempData, 0, length);
}
data = tempData;
}
data[length++] = n;
}
}
static class NextWord {
Node word;
NextWord nextWord;
int frequency;
NextWord(Node word, int frequency) {
this.word = word;
this.frequency = frequency;
}
}
private NodeArray mRoots;
private int[][] mCodes;
ExpandableDictionary(Context context, int dicTypeId) {
mContext = context;
clearDictionary();
mCodes = new int[MAX_WORD_LENGTH][];
mDicTypeId = dicTypeId;
}
public void loadDictionary() {
synchronized (mUpdatingLock) {
startDictionaryLoadingTaskLocked();
}
}
public void startDictionaryLoadingTaskLocked() {
if (!mUpdatingDictionary) {
mUpdatingDictionary = true;
mRequiresReload = false;
new LoadDictionaryTask().execute();
}
}
public void setRequiresReload(boolean reload) {
synchronized (mUpdatingLock) {
mRequiresReload = reload;
}
}
public boolean getRequiresReload() {
return mRequiresReload;
}
/** Override to load your dictionary here, on a background thread. */
public void loadDictionaryAsync() {
}
Context getContext() {
return mContext;
}
int getMaxWordLength() {
return MAX_WORD_LENGTH;
}
public void addWord(String word, int frequency) {
addWordRec(mRoots, word, 0, frequency, null);
}
private void addWordRec(NodeArray children, final String word, final int depth,
final int frequency, Node parentNode) {
final int wordLength = word.length();
final char c = word.charAt(depth);
// Does children have the current character?
final int childrenLength = children.length;
Node childNode = null;
boolean found = false;
for (int i = 0; i < childrenLength; i++) {
childNode = children.data[i];
if (childNode.code == c) {
found = true;
break;
}
}
if (!found) {
childNode = new Node();
childNode.code = c;
childNode.parent = parentNode;
children.add(childNode);
}
if (wordLength == depth + 1) {
// Terminate this word
childNode.terminal = true;
childNode.frequency = Math.max(frequency, childNode.frequency);
if (childNode.frequency > 255) childNode.frequency = 255;
return;
}
if (childNode.children == null) {
childNode.children = new NodeArray();
}
addWordRec(childNode.children, word, depth + 1, frequency, childNode);
}
@Override
public void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
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;
}
mInputLength = codes.size();
mNextLettersFrequencies = nextLettersFrequencies;
if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
// Cache the codes so that we don't have to lookup an array list
for (int i = 0; i < mInputLength; i++) {
mCodes[i] = codes.getCodesAt(i);
}
mMaxDepth = mInputLength * 3;
getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, callback);
for (int i = 0; i < mInputLength; i++) {
getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, callback);
}
}
@Override
public synchronized boolean isValidWord(CharSequence word) {
synchronized (mUpdatingLock) {
// If we need to update, start off a background task
if (mRequiresReload) startDictionaryLoadingTaskLocked();
if (mUpdatingDictionary) return false;
}
final int freq = getWordFrequency(word);
return freq > -1;
}
/**
* Returns the word's frequency or -1 if not found
*/
public int getWordFrequency(CharSequence word) {
Node node = searchNode(mRoots, word, 0, word.length());
return (node == null) ? -1 : node.frequency;
}
/**
* Recursively traverse the tree for words that match the input. Input consists of
* a list of arrays. Each item in the list is one input character position. An input
* character is actually an array of multiple possible candidates. This function is not
* optimized for speed, assuming that the user dictionary will only be a few hundred words in
* size.
* @param roots node whose children have to be search for matches
* @param codes the input character codes
* @param word the word being composed as a possible match
* @param depth the depth of traversal - the length of the word being composed thus far
* @param completion whether the traversal is now in completion mode - meaning that we've
* exhausted the input and we're looking for all possible suffixes.
* @param snr current weight of the word being formed
* @param inputIndex position in the input characters. This can be off from the depth in
* case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
* "wouldve", it could be matching "would've", so the depth will be one more than the
* inputIndex
* @param callback the callback class for adding a word
*/
protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
final int depth, boolean completion, int snr, int inputIndex, int skipPos,
WordCallback callback) {
final int count = roots.length;
final int codeSize = mInputLength;
// Optimization: Prune out words that are too long compared to how much was typed.
if (depth > mMaxDepth) {
return;
}
int[] currentChars = null;
if (codeSize <= inputIndex) {
completion = true;
} else {
currentChars = mCodes[inputIndex];
}
for (int i = 0; i < count; i++) {
final Node node = roots.data[i];
final char c = node.code;
final char lowerC = toLowerCase(c);
final boolean terminal = node.terminal;
final NodeArray children = node.children;
final int freq = node.frequency;
if (completion) {
word[depth] = c;
if (terminal) {
if (!callback.addWord(word, 0, depth + 1, freq * snr, mDicTypeId,
DataType.UNIGRAM)) {
return;
}
// Add to frequency of next letters for predictive correction
if (mNextLettersFrequencies != null && depth >= inputIndex && skipPos < 0
&& mNextLettersFrequencies.length > word[inputIndex]) {
mNextLettersFrequencies[word[inputIndex]]++;
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
skipPos, callback);
}
} else if ((c == QUOTE && currentChars[0] != QUOTE) || depth == skipPos) {
// Skip the ' and continue deeper
word[depth] = c;
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
skipPos, callback);
}
} else {
// Don't use alternatives if we're looking for missing characters
final int alternativesSize = skipPos >= 0? 1 : currentChars.length;
for (int j = 0; j < alternativesSize; j++) {
final int addedAttenuation = (j > 0 ? 1 : 2);
final int currentChar = currentChars[j];
if (currentChar == -1) {
break;
}
if (currentChar == lowerC || currentChar == c) {
word[depth] = c;
if (codeSize == inputIndex + 1) {
if (terminal) {
if (INCLUDE_TYPED_WORD_IF_VALID
|| !same(word, depth + 1, codes.getTypedWord())) {
int finalFreq = freq * snr * addedAttenuation;
if (skipPos < 0) finalFreq *= FULL_WORD_FREQ_MULTIPLIER;
callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
DataType.UNIGRAM);
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1,
true, snr * addedAttenuation, inputIndex + 1,
skipPos, callback);
}
} else if (children != null) {
getWordsRec(children, codes, word, depth + 1,
false, snr * addedAttenuation, inputIndex + 1,
skipPos, callback);
}
}
}
}
}
}
protected int setBigram(String word1, String word2, int frequency) {
return addOrSetBigram(word1, word2, frequency, false);
}
protected int addBigram(String word1, String word2, int frequency) {
return addOrSetBigram(word1, word2, frequency, true);
}
/**
* Adds bigrams to the in-memory trie structure that is being used to retrieve any word
* @param frequency frequency for this bigrams
* @param addFrequency if true, it adds to current frequency
* @return returns the final frequency
*/
private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) {
Node firstWord = searchWord(mRoots, word1, 0, null);
Node secondWord = searchWord(mRoots, word2, 0, null);
LinkedList<NextWord> bigram = firstWord.ngrams;
if (bigram == null || bigram.size() == 0) {
firstWord.ngrams = new LinkedList<NextWord>();
bigram = firstWord.ngrams;
} else {
for (NextWord nw : bigram) {
if (nw.word == secondWord) {
if (addFrequency) {
nw.frequency += frequency;
} else {
nw.frequency = frequency;
}
return nw.frequency;
}
}
}
NextWord nw = new NextWord(secondWord, frequency);
firstWord.ngrams.add(nw);
return frequency;
}
/**
* Searches for the word and add the word if it does not exist.
* @return Returns the terminal node of the word we are searching for.
*/
private Node searchWord(NodeArray children, String word, int depth, Node parentNode) {
final int wordLength = word.length();
final char c = word.charAt(depth);
// Does children have the current character?
final int childrenLength = children.length;
Node childNode = null;
boolean found = false;
for (int i = 0; i < childrenLength; i++) {
childNode = children.data[i];
if (childNode.code == c) {
found = true;
break;
}
}
if (!found) {
childNode = new Node();
childNode.code = c;
childNode.parent = parentNode;
children.add(childNode);
}
if (wordLength == depth + 1) {
// Terminate this word
childNode.terminal = true;
return childNode;
}
if (childNode.children == null) {
childNode.children = new NodeArray();
}
return searchWord(childNode.children, 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 runReverseLookUp(final CharSequence previousWord, final WordCallback callback) {
Node prevWord = searchNode(mRoots, previousWord, 0, previousWord.length());
if (prevWord != null && prevWord.ngrams != null) {
reverseLookUp(prevWord.ngrams, callback);
}
}
@Override
public void getBigrams(final WordComposer codes, final CharSequence previousWord,
final WordCallback callback, int[] nextLettersFrequencies) {
if (!reloadDictionaryIfRequired()) {
runReverseLookUp(previousWord, callback);
}
}
/**
* Used only for testing purposes
* This function will wait for loading from database to be done
*/
void waitForDictionaryLoading() {
while (mUpdatingDictionary) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
/**
* reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
* through callback.
* @param terminalNodes list of terminal nodes we want to add
*/
private void reverseLookUp(LinkedList<NextWord> terminalNodes,
final WordCallback callback) {
Node node;
int freq;
for (NextWord nextWord : terminalNodes) {
node = nextWord.word;
freq = nextWord.frequency;
// TODO Not the best way to limit suggestion threshold
if (freq >= UserBigramDictionary.SUGGEST_THRESHOLD) {
sb.setLength(0);
do {
sb.insert(0, node.code);
node = node.parent;
} while(node != null);
// TODO better way to feed char array?
callback.addWord(sb.toString().toCharArray(), 0, sb.length(), freq, mDicTypeId,
DataType.BIGRAM);
}
}
}
/**
* Search for the terminal node of the word
* @return Returns the terminal node of the word if the word exists
*/
private Node searchNode(final NodeArray children, final CharSequence word, final int offset,
final int length) {
// TODO Consider combining with addWordRec
final int count = children.length;
char currentChar = word.charAt(offset);
for (int j = 0; j < count; j++) {
final Node node = children.data[j];
if (node.code == currentChar) {
if (offset == length - 1) {
if (node.terminal) {
return node;
}
} else {
if (node.children != null) {
Node returnNode = searchNode(node.children, word, offset + 1, length);
if (returnNode != null) return returnNode;
}
}
}
}
return null;
}
protected void clearDictionary() {
mRoots = new NodeArray();
}
private class LoadDictionaryTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... v) {
loadDictionaryAsync();
synchronized (mUpdatingLock) {
mUpdatingDictionary = false;
}
return null;
}
}
static char toLowerCase(char c) {
if (c < BASE_CHARS.length) {
c = BASE_CHARS[c];
}
if (c >= 'A' && c <= 'Z') {
c = (char) (c | 32);
} else if (c > 127) {
c = Character.toLowerCase(c);
}
return c;
}
/**
* Table mapping most combined Latin, Greek, and Cyrillic characters
* to their base characters. If c is in range, BASE_CHARS[c] == c
* if c is not a combined character, or the base character if it
* is combined.
*/
static final char BASE_CHARS[] = {
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f,
0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f,
0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7,
0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020,
0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7,
0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf,
0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043,
0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7,
0x004f, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00de, 0x0073, // Manually changed d8 to 4f
// Manually changed df to 73
0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7,
0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, // Manually changed f8 to 6f
0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063,
0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064,
0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065,
0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067,
0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127,
0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069,
0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b,
0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c,
0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e,
0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f,
0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072,
0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073,
0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167,
0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075,
0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079,
0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073,
0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187,
0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f,
0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197,
0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f,
0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7,
0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055,
0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7,
0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf,
0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c,
0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049,
0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc,
0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4,
0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067,
0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292,
0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7,
0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8,
0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065,
0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f,
0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075,
0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068,
0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061,
0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f,
0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f,
0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247,
0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f,
0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f,
0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f,
0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f,
0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f,
0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f,
0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7,
0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af,
0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077,
0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf,
0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7,
0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf,
0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7,
0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df,
0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7,
0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef,
0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7,
0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff,
0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307,
0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f,
0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317,
0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f,
0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327,
0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f,
0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337,
0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f,
0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347,
0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f,
0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f,
0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f,
0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377,
0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f,
0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7,
0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9,
0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f,
0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7,
0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9,
0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf,
0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7,
0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df,
0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7,
0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef,
0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7,
0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff,
0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406,
0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f,
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f,
0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f,
0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467,
0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f,
0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475,
0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f,
0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f,
0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497,
0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f,
0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7,
0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af,
0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7,
0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf,
0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7,
0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf,
0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435,
0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437,
0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e,
0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443,
0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7,
0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff,
};
// generated with:
// cat UnicodeData.txt | perl -e 'while (<>) { @foo = split(/;/); $foo[5] =~ s/<.*> //; $base[hex($foo[0])] = hex($foo[5]);} for ($i = 0; $i < 0x500; $i += 8) { for ($j = $i; $j < $i + 8; $j++) { printf("0x%04x, ", $base[$j] ? $base[$j] : $j)}; print "\n"; }'
}

View File

@ -0,0 +1,164 @@
/*
* Copyright (C) 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.inputmethod.InputConnection;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* Logic to determine when to display hints on usage to the user.
*/
public class Hints {
public interface Display {
public void showHint(int viewResource);
}
private static final String PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN =
"voice_hint_num_unique_days_shown";
private static final String PREF_VOICE_HINT_LAST_TIME_SHOWN =
"voice_hint_last_time_shown";
private static final String PREF_VOICE_INPUT_LAST_TIME_USED =
"voice_input_last_time_used";
private static final String PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT =
"voice_punctuation_hint_view_count";
private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7;
private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7;
private Context mContext;
private Display mDisplay;
private boolean mVoiceResultContainedPunctuation;
private int mSwipeHintMaxDaysToShow;
private int mPunctuationHintMaxDisplays;
// Only show punctuation hint if voice result did not contain punctuation.
static final Map<CharSequence, String> SPEAKABLE_PUNCTUATION
= new HashMap<CharSequence, String>();
static {
SPEAKABLE_PUNCTUATION.put(",", "comma");
SPEAKABLE_PUNCTUATION.put(".", "period");
SPEAKABLE_PUNCTUATION.put("?", "question mark");
}
public Hints(Context context, Display display) {
mContext = context;
mDisplay = display;
ContentResolver cr = mContext.getContentResolver();
}
public boolean showSwipeHintIfNecessary(boolean fieldRecommended) {
if (fieldRecommended && shouldShowSwipeHint()) {
showHint(R.layout.voice_swipe_hint);
return true;
}
return false;
}
public boolean showPunctuationHintIfNecessary(InputConnection ic) {
if (!mVoiceResultContainedPunctuation
&& ic != null
&& getAndIncrementPref(PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT)
< mPunctuationHintMaxDisplays) {
CharSequence charBeforeCursor = ic.getTextBeforeCursor(1, 0);
if (SPEAKABLE_PUNCTUATION.containsKey(charBeforeCursor)) {
showHint(R.layout.voice_punctuation_hint);
return true;
}
}
return false;
}
public void registerVoiceResult(String text) {
// Update the current time as the last time voice input was used.
SharedPreferences.Editor editor =
PreferenceManager.getDefaultSharedPreferences(mContext).edit();
editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis());
SharedPreferencesCompat.apply(editor);
mVoiceResultContainedPunctuation = false;
for (CharSequence s : SPEAKABLE_PUNCTUATION.keySet()) {
if (text.indexOf(s.toString()) >= 0) {
mVoiceResultContainedPunctuation = true;
break;
}
}
}
private boolean shouldShowSwipeHint() {
return false;
}
/**
* Determines whether the provided time is from some time today (i.e., this day, month,
* and year).
*/
private boolean isFromToday(long timeInMillis) {
if (timeInMillis == 0) return false;
Calendar today = Calendar.getInstance();
today.setTimeInMillis(System.currentTimeMillis());
Calendar timestamp = Calendar.getInstance();
timestamp.setTimeInMillis(timeInMillis);
return (today.get(Calendar.YEAR) == timestamp.get(Calendar.YEAR) &&
today.get(Calendar.DAY_OF_MONTH) == timestamp.get(Calendar.DAY_OF_MONTH) &&
today.get(Calendar.MONTH) == timestamp.get(Calendar.MONTH));
}
private void showHint(int hintViewResource) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
int numUniqueDaysShown = sp.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
long lastTimeHintWasShown = sp.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0);
// If this is the first time the hint is being shown today, increase the saved values
// to represent that. We don't need to increase the last time the hint was shown unless
// it is a different day from the current value.
if (!isFromToday(lastTimeHintWasShown)) {
SharedPreferences.Editor editor = sp.edit();
editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1);
editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis());
SharedPreferencesCompat.apply(editor);
}
if (mDisplay != null) {
mDisplay.showHint(hintViewResource);
}
}
private int getAndIncrementPref(String pref) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
int value = sp.getInt(pref, 0);
SharedPreferences.Editor editor = sp.edit();
editor.putInt(pref, value + 1);
SharedPreferencesCompat.apply(editor);
return value;
}
}

View File

@ -0,0 +1,250 @@
/*
* Copyright (C) 2008-2009 Google Inc.
* Copyright (C) 2014 Philipp Crocoll <crocoapps@googlemail.com>
* Copyright (C) 2014 Wiktor Lawski <wiktor.lawski@gmail.com>
*
* 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 keepass2android.softkeyboard;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
public class InputLanguageSelection extends PreferenceActivity {
private String mSelectedLanguages;
private ArrayList<Loc> mAvailableLanguages = new ArrayList<Loc>();
private static final String[] WHITELIST_LANGUAGES = {
"cs", "da", "de", "en_GB", "en_US", "es", "es_US", "fr", "it", "nb", "nl", "pl", "pt",
"ru", "tr"
};
private static final String[] WEAK_WHITELIST_LANGUAGES = {
"cs", "da", "de", "en_GB", "en_US", "es", "es_US", "fr", "it", "nb", "nl", "pl", "pt",
"ru", "tr", "en"
};
private static boolean isWhitelisted(String lang, boolean strict) {
for (String s : (strict? WHITELIST_LANGUAGES : WEAK_WHITELIST_LANGUAGES)) {
if (s.equalsIgnoreCase(lang)) {
return true;
}
if ((!strict) && (s.length()==2) && lang.toLowerCase(Locale.US).startsWith(s))
{
return true;
}
}
return false;
}
private static class Loc implements Comparable<Object> {
static Collator sCollator = Collator.getInstance();
String label;
Locale locale;
public Loc(String label, Locale locale) {
this.label = label;
this.locale = locale;
}
@Override
public String toString() {
return this.label;
}
public int compareTo(Object o) {
return sCollator.compare(this.label, ((Loc) o).label);
}
}
@Override
protected void onCreate(Bundle icicle) {
// Get the settings preferences
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
Design.updateTheme(this, sp);
super.onCreate(icicle);
addPreferencesFromResource(R.xml.language_prefs);
mSelectedLanguages = sp.getString(KP2AKeyboard.PREF_SELECTED_LANGUAGES, "");
String[] languageList = mSelectedLanguages.split(",");
//first try to get the unique locales in a strict mode (filtering most redundant layouts like English (Jamaica) etc.)
mAvailableLanguages = getUniqueLocales(true);
//sometimes the strict check returns only EN_US, EN_GB and ES_US. Accept more in these cases:
if (mAvailableLanguages.size() < 5)
{
mAvailableLanguages = getUniqueLocales(false);
}
PreferenceGroup parent = getPreferenceScreen();
for (int i = 0; i < mAvailableLanguages.size(); i++) {
CheckBoxPreference pref = new CheckBoxPreference(this);
Locale locale = mAvailableLanguages.get(i).locale;
pref.setTitle(LanguageSwitcher.toTitleCase(locale.getDisplayName(locale), locale));
boolean checked = isLocaleIn(locale, languageList);
pref.setChecked(checked);
if (hasDictionary(locale, this)) {
pref.setSummary(R.string.has_dictionary);
}
parent.addPreference(pref);
}
}
private boolean isLocaleIn(Locale locale, String[] list) {
String lang = get5Code(locale);
for (int i = 0; i < list.length; i++) {
if (lang.equalsIgnoreCase(list[i])) return true;
}
return false;
}
private boolean hasDictionary(Locale locale, Context ctx) {
Resources res = getResources();
Configuration conf = res.getConfiguration();
Locale saveLocale = conf.locale;
boolean haveDictionary = false;
conf.locale = locale;
res.updateConfiguration(conf, res.getDisplayMetrics());
//somewhat a hack. But simply querying the dictionary will always return an English
//dictionary in KP2A so if we get a dict, we wouldn't know if it's language specific
if (locale.getLanguage().equals("en"))
{
haveDictionary = true;
}
else
{
BinaryDictionary plug = PluginManager.getDictionary(getApplicationContext(), locale.getLanguage());
if (plug != null) {
plug.close();
haveDictionary = true;
}
}
conf.locale = saveLocale;
res.updateConfiguration(conf, res.getDisplayMetrics());
return haveDictionary;
}
private String get5Code(Locale locale) {
String country = locale.getCountry();
return locale.getLanguage()
+ (TextUtils.isEmpty(country) ? "" : "_" + country);
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
// Save the selected languages
String checkedLanguages = "";
PreferenceGroup parent = getPreferenceScreen();
int count = parent.getPreferenceCount();
for (int i = 0; i < count; i++) {
CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i);
if (pref.isChecked()) {
Locale locale = mAvailableLanguages.get(i).locale;
checkedLanguages += get5Code(locale) + ",";
}
}
if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
Editor editor = sp.edit();
editor.putString(KP2AKeyboard.PREF_SELECTED_LANGUAGES, checkedLanguages);
SharedPreferencesCompat.apply(editor);
}
ArrayList<Loc> getUniqueLocales(boolean strict) {
String[] locales = getAssets().getLocales();
Arrays.sort(locales);
ArrayList<Loc> uniqueLocales = new ArrayList<Loc>();
final int origSize = locales.length;
Loc[] preprocess = new Loc[origSize];
int finalSize = 0;
for (int i = 0 ; i < origSize; i++ ) {
String s = locales[i];
int len = s.length();
final Locale l;
final String language;
if (len == 5) {
language = s.substring(0, 2);
String country = s.substring(3, 5);
l = new Locale(language, country);
} else if (len == 2) {
language = s;
l = new Locale(language);
} else {
android.util.Log.d("KP2AK", "locale "+s+" has unexpected length.");
continue;
}
// Exclude languages that are not relevant to LatinIME
if (!isWhitelisted(s, strict))
{
android.util.Log.d("KP2AK", "locale "+s+" is not white-listed");
continue;
}
android.util.Log.d("KP2AK", "adding locale "+s);
if (finalSize == 0) {
preprocess[finalSize++] =
new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(l), l), l);
} else {
// check previous entry:
// same lang and a country -> upgrade to full name and
// insert ours with full name
// diff lang -> insert ours with lang-only name
if (preprocess[finalSize-1].locale.getLanguage().equals(
language)) {
preprocess[finalSize-1].label = LanguageSwitcher.toTitleCase(
preprocess[finalSize-1].locale.getDisplayName(),
preprocess[finalSize-1].locale);
preprocess[finalSize++] =
new Loc(LanguageSwitcher.toTitleCase(l.getDisplayName(), l), l);
} else {
String displayName;
if (s.equals("zz_ZZ")) {
} else {
displayName = LanguageSwitcher.toTitleCase(l.getDisplayName(l), l);
preprocess[finalSize++] = new Loc(displayName, l);
}
}
}
}
for (int i = 0; i < finalSize ; i++) {
uniqueLocales.add(preprocess[i]);
}
return uniqueLocales;
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.Keyboard.Key;
import java.util.Arrays;
import java.util.List;
abstract class KeyDetector {
protected Keyboard mKeyboard;
private Key[] mKeys;
protected int mCorrectionX;
protected int mCorrectionY;
protected boolean mProximityCorrectOn;
protected int mProximityThresholdSquare;
public Key[] setKeyboard(Keyboard keyboard, float correctionX, float correctionY) {
if (keyboard == null)
throw new NullPointerException();
mCorrectionX = (int)correctionX;
mCorrectionY = (int)correctionY;
mKeyboard = keyboard;
List<Key> keys = mKeyboard.getKeys();
Key[] array = keys.toArray(new Key[keys.size()]);
mKeys = array;
return array;
}
protected int getTouchX(int x) {
return x + mCorrectionX;
}
protected int getTouchY(int y) {
return y + mCorrectionY;
}
protected Key[] getKeys() {
if (mKeys == null)
throw new IllegalStateException("keyboard isn't set");
// mKeyboard is guaranteed not to be null at setKeybaord() method if mKeys is not null
return mKeys;
}
public void setProximityCorrectionEnabled(boolean enabled) {
mProximityCorrectOn = enabled;
}
public boolean isProximityCorrectionEnabled() {
return mProximityCorrectOn;
}
public void setProximityThreshold(int threshold) {
mProximityThresholdSquare = threshold * threshold;
}
/**
* Allocates array that can hold all key indices returned by {@link #getKeyIndexAndNearbyCodes}
* method. The maximum size of the array should be computed by {@link #getMaxNearbyKeys}.
*
* @return Allocates and returns an array that can hold all key indices returned by
* {@link #getKeyIndexAndNearbyCodes} method. All elements in the returned array are
* initialized by {@link keepass2android.softkeyboard.LatinKeyboardView.NOT_A_KEY}
* value.
*/
public int[] newCodeArray() {
int[] codes = new int[getMaxNearbyKeys()];
Arrays.fill(codes, LatinKeyboardBaseView.NOT_A_KEY);
return codes;
}
/**
* Computes maximum size of the array that can contain all nearby key indices returned by
* {@link #getKeyIndexAndNearbyCodes}.
*
* @return Returns maximum size of the array that can contain all nearby key indices returned
* by {@link #getKeyIndexAndNearbyCodes}.
*/
abstract protected int getMaxNearbyKeys();
/**
* Finds all possible nearby key indices around a touch event point and returns the nearest key
* index. The algorithm to determine the nearby keys depends on the threshold set by
* {@link #setProximityThreshold(int)} and the mode set by
* {@link #setProximityCorrectionEnabled(boolean)}.
*
* @param x The x-coordinate of a touch point
* @param y The y-coordinate of a touch point
* @param allKeys All nearby key indices are returned in this array
* @return The nearest key index
*/
abstract public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys);
}

View File

@ -0,0 +1,591 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.InflateException;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final int MODE_NONE = 0;
public static final int MODE_TEXT = 1;
public static final int MODE_SYMBOLS = 2;
public static final int MODE_PHONE = 3;
public static final int MODE_URL = 4;
public static final int MODE_EMAIL = 5;
public static final int MODE_IM = 6;
public static final int MODE_WEB = 7;
public static final int MODE_KP2A = 8;
// Main keyboard layouts without the settings key
public static final int KEYBOARDMODE_NORMAL = R.id.mode_normal;
public static final int KEYBOARDMODE_URL = R.id.mode_url;
public static final int KEYBOARDMODE_EMAIL = R.id.mode_email;
public static final int KEYBOARDMODE_IM = R.id.mode_im;
public static final int KEYBOARDMODE_WEB = R.id.mode_webentry;
// Main keyboard layouts with the settings key
public static final int KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY =
R.id.mode_normal_with_settings_key;
public static final int KEYBOARDMODE_URL_WITH_SETTINGS_KEY =
R.id.mode_url_with_settings_key;
public static final int KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY =
R.id.mode_email_with_settings_key;
public static final int KEYBOARDMODE_IM_WITH_SETTINGS_KEY =
R.id.mode_im_with_settings_key;
public static final int KEYBOARDMODE_WEB_WITH_SETTINGS_KEY =
R.id.mode_webentry_with_settings_key;
// Symbols keyboard layout without the settings key
public static final int KEYBOARDMODE_SYMBOLS = R.id.mode_symbols;
// Symbols keyboard layout with the settings key
public static final int KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY =
R.id.mode_symbols_with_settings_key;
public static final String DEFAULT_LAYOUT_ID = "4";
public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902";
private static final int[] THEMES = new int [] {
R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal,
R.layout.input_stone_bold, R.layout.input_gingerbread};
// Ids for each characters' color in the keyboard
private static final int CHAR_THEME_COLOR_WHITE = 0;
private static final int CHAR_THEME_COLOR_BLACK = 1;
// Tables which contains resource ids for each character theme color
private static final int[] KBD_PHONE = new int[] {R.xml.kbd_phone, R.xml.kbd_phone_black};
private static final int[] KBD_PHONE_SYMBOLS = new int[] {
R.xml.kbd_phone_symbols, R.xml.kbd_phone_symbols_black};
private static final int[] KBD_SYMBOLS = new int[] {
R.xml.kbd_symbols, R.xml.kbd_symbols_black};
private static final int[] KBD_SYMBOLS_SHIFT = new int[] {
R.xml.kbd_symbols_shift, R.xml.kbd_symbols_shift_black};
private static final int[] KBD_QWERTY = new int[] {R.xml.kbd_qwerty, R.xml.kbd_qwerty_black};
private static final int[] KBD_KP2A = new int[] {R.xml.kbd_kp2a, R.xml.kbd_kp2a_black};
private LatinKeyboardView mInputView;
private static final int[] ALPHABET_MODES = {
KEYBOARDMODE_NORMAL,
KEYBOARDMODE_URL,
KEYBOARDMODE_EMAIL,
KEYBOARDMODE_IM,
KEYBOARDMODE_WEB,
KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY,
KEYBOARDMODE_URL_WITH_SETTINGS_KEY,
KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY,
KEYBOARDMODE_IM_WITH_SETTINGS_KEY,
KEYBOARDMODE_WEB_WITH_SETTINGS_KEY };
private KP2AKeyboard mInputMethodService;
private KeyboardId mSymbolsId;
private KeyboardId mSymbolsShiftedId;
private KeyboardId mCurrentId;
private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboards =
new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
private int mMode = MODE_NONE; /** One of the MODE_XXX values */
private int mImeOptions;
private boolean mIsSymbols;
/** mIsAutoCompletionActive indicates that auto completed word will be input instead of
* what user actually typed. */
private boolean mIsAutoCompletionActive;
private boolean mPreferSymbols;
private static final int AUTO_MODE_SWITCH_STATE_ALPHA = 0;
private static final int AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN = 1;
private static final int AUTO_MODE_SWITCH_STATE_SYMBOL = 2;
// The following states are used only on the distinct multi-touch panel devices.
private static final int AUTO_MODE_SWITCH_STATE_MOMENTARY = 3;
private static final int AUTO_MODE_SWITCH_STATE_CHORDING = 4;
private int mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
// Indicates whether or not we have the settings key
private boolean mHasSettingsKey;
private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto;
private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW = R.string.settings_key_mode_always_show;
// NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to
// in the source code now.
// Default is SETTINGS_KEY_MODE_AUTO.
private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO;
private int mLastDisplayWidth;
private LanguageSwitcher mLanguageSwitcher;
private Locale mInputLocale;
private int mLayoutId;
private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
public static KeyboardSwitcher getInstance() {
return sInstance;
}
private KeyboardSwitcher() {
// Intentional empty constructor for singleton.
}
public static void init(KP2AKeyboard ims) {
sInstance.mInputMethodService = ims;
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ims);
sInstance.mLayoutId = Integer.valueOf(
prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID));
sInstance.updateSettingsKeyState(prefs);
prefs.registerOnSharedPreferenceChangeListener(sInstance);
sInstance.mSymbolsId = sInstance.makeSymbolsId();
sInstance.mSymbolsShiftedId = sInstance.makeSymbolsShiftedId();
}
/**
* Sets the input locale, when there are multiple locales for input.
* If no locale switching is required, then the locale should be set to null.
* @param locale the current input locale, or null for default locale with no locale
* button.
*/
public void setLanguageSwitcher(LanguageSwitcher languageSwitcher) {
mLanguageSwitcher = languageSwitcher;
mInputLocale = mLanguageSwitcher.getInputLocale();
}
private KeyboardId makeSymbolsId() {
return new KeyboardId(KBD_SYMBOLS[getCharColorId()], mHasSettingsKey ?
KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
false);
}
private KeyboardId makeSymbolsShiftedId() {
return new KeyboardId(KBD_SYMBOLS_SHIFT[getCharColorId()], mHasSettingsKey ?
KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
false);
}
public void makeKeyboards(boolean forceCreate) {
mSymbolsId = makeSymbolsId();
mSymbolsShiftedId = makeSymbolsShiftedId();
if (forceCreate) mKeyboards.clear();
// Configuration change is coming after the keyboard gets recreated. So don't rely on that.
// If keyboards have already been made, check if we have a screen width change and
// create the keyboard layouts again at the correct orientation
int displayWidth = mInputMethodService.getMaxWidth();
if (displayWidth == mLastDisplayWidth) return;
mLastDisplayWidth = displayWidth;
if (!forceCreate) mKeyboards.clear();
}
/**
* Represents the parameters necessary to construct a new LatinKeyboard,
* which also serve as a unique identifier for each keyboard type.
*/
private static class KeyboardId {
// TODO: should have locale and portrait/landscape orientation?
public final int mXml;
public final int mKeyboardMode; /** A KEYBOARDMODE_XXX value */
public final boolean mEnableShiftLock;
private final int mHashCode;
public KeyboardId(int xml, int mode, boolean enableShiftLock) {
this.mXml = xml;
this.mKeyboardMode = mode;
this.mEnableShiftLock = enableShiftLock;
this.mHashCode = Arrays.hashCode(new Object[] {
xml, mode, enableShiftLock
});
}
public KeyboardId(int xml) {
this(xml, 0, false);
}
@Override
public boolean equals(Object other) {
return other instanceof KeyboardId && equals((KeyboardId) other);
}
private boolean equals(KeyboardId other) {
return other.mXml == this.mXml
&& other.mKeyboardMode == this.mKeyboardMode
&& other.mEnableShiftLock == this.mEnableShiftLock
;
}
@Override
public int hashCode() {
return mHashCode;
}
}
public void setKeyboardMode(int mode, int imeOptions) {
Log.d("KP2AK", "Switcher.SetKeyboardMode: " + mode);
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
mPreferSymbols = mode == MODE_SYMBOLS;
if (mode == MODE_SYMBOLS) {
mode = MODE_TEXT;
}
try {
setKeyboardMode(mode, imeOptions, mPreferSymbols);
} catch (RuntimeException e) {
LatinImeLogger.logOnException(mode + "," + imeOptions + "," + mPreferSymbols, e);
}
}
private void setKeyboardMode(int mode, int imeOptions, boolean isSymbols) {
if (mInputView == null) return;
mMode = mode;
mImeOptions = imeOptions;
mIsSymbols = isSymbols;
mInputView.setPreviewEnabled(mInputMethodService.getPopupOn());
KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols);
LatinKeyboard keyboard = null;
keyboard = getKeyboard(id);
if (mode == MODE_PHONE) {
mInputView.setPhoneKeyboard(keyboard);
}
mCurrentId = id;
mInputView.setKeyboard(keyboard);
keyboard.setShifted(false);
keyboard.setShiftLocked(keyboard.isShiftLocked());
keyboard.setImeOptions(mInputMethodService.getResources(), mMode, imeOptions);
keyboard.setColorOfSymbolIcons(mIsAutoCompletionActive, isBlackSym());
// Update the settings key state because number of enabled IMEs could have been changed
updateSettingsKeyState(PreferenceManager.getDefaultSharedPreferences(mInputMethodService));
}
private LatinKeyboard getKeyboard(KeyboardId id) {
SoftReference<LatinKeyboard> ref = mKeyboards.get(id);
LatinKeyboard keyboard = (ref == null) ? null : ref.get();
if (keyboard == null) {
Resources orig = mInputMethodService.getResources();
Configuration conf = orig.getConfiguration();
Locale saveLocale = conf.locale;
conf.locale = mInputLocale;
orig.updateConfiguration(conf, null);
keyboard = new LatinKeyboard(mInputMethodService, id.mXml, id.mKeyboardMode);
keyboard.setLanguageSwitcher(mLanguageSwitcher, mIsAutoCompletionActive, isBlackSym());
if (id.mEnableShiftLock) {
keyboard.enableShiftLock();
}
mKeyboards.put(id, new SoftReference<LatinKeyboard>(keyboard));
conf.locale = saveLocale;
orig.updateConfiguration(conf, null);
}
return keyboard;
}
private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) {
int charColorId = getCharColorId();
if (isSymbols) {
if (mode == MODE_PHONE) {
return new KeyboardId(KBD_PHONE_SYMBOLS[charColorId]);
} else {
return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ?
KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
false);
}
}
// TODO: generalize for any KeyboardId
int keyboardRowsResId = KBD_QWERTY[charColorId];
switch (mode) {
case MODE_KP2A:
return new KeyboardId(KBD_KP2A[charColorId]);
case MODE_NONE:
LatinImeLogger.logOnWarning(
"getKeyboardId:" + mode + "," + imeOptions + "," + isSymbols);
/* fall through */
case MODE_TEXT:
return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
KEYBOARDMODE_NORMAL_WITH_SETTINGS_KEY : KEYBOARDMODE_NORMAL,
true);
case MODE_SYMBOLS:
return new KeyboardId(KBD_SYMBOLS[charColorId], mHasSettingsKey ?
KEYBOARDMODE_SYMBOLS_WITH_SETTINGS_KEY : KEYBOARDMODE_SYMBOLS,
false);
case MODE_PHONE:
return new KeyboardId(KBD_PHONE[charColorId]);
case MODE_URL:
return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
KEYBOARDMODE_URL_WITH_SETTINGS_KEY : KEYBOARDMODE_URL, true);
case MODE_EMAIL:
return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
KEYBOARDMODE_EMAIL_WITH_SETTINGS_KEY : KEYBOARDMODE_EMAIL, true);
case MODE_IM:
return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
KEYBOARDMODE_IM_WITH_SETTINGS_KEY : KEYBOARDMODE_IM, true);
case MODE_WEB:
return new KeyboardId(keyboardRowsResId, mHasSettingsKey ?
KEYBOARDMODE_WEB_WITH_SETTINGS_KEY : KEYBOARDMODE_WEB, true);
}
return null;
}
public int getKeyboardMode() {
return mMode;
}
public boolean isAlphabetMode() {
if (mCurrentId == null) {
return false;
}
int currentMode = mCurrentId.mKeyboardMode;
for (Integer mode : ALPHABET_MODES) {
if (currentMode == mode) {
return true;
}
}
return false;
}
public void setShifted(boolean shifted) {
if (mInputView != null) {
mInputView.setShifted(shifted);
}
}
public void setShiftLocked(boolean shiftLocked) {
if (mInputView != null) {
mInputView.setShiftLocked(shiftLocked);
}
}
public void toggleShift() {
if (isAlphabetMode())
return;
if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) {
LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId);
mCurrentId = mSymbolsShiftedId;
mInputView.setKeyboard(symbolsShiftedKeyboard);
// Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To
// enable the indicator, we need to call enableShiftLock() and setShiftLocked(true).
// Thus we can keep the ALT key's Key.on value true while LatinKey.onRelease() is
// called.
symbolsShiftedKeyboard.enableShiftLock();
symbolsShiftedKeyboard.setShiftLocked(true);
symbolsShiftedKeyboard.setImeOptions(mInputMethodService.getResources(),
mMode, mImeOptions);
} else {
LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId);
mCurrentId = mSymbolsId;
mInputView.setKeyboard(symbolsKeyboard);
// Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the
// indicator, we need to call enableShiftLock() and setShiftLocked(false).
symbolsKeyboard.enableShiftLock();
symbolsKeyboard.setShifted(false);
symbolsKeyboard.setImeOptions(mInputMethodService.getResources(), mMode, mImeOptions);
}
}
public void onCancelInput() {
// Snap back to the previous keyboard mode if the user cancels sliding input.
if (mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_MOMENTARY && getPointerCount() == 1)
mInputMethodService.changeKeyboardMode();
}
public void toggleSymbols() {
setKeyboardMode(mMode, mImeOptions, !mIsSymbols);
if (mIsSymbols && !mPreferSymbols) {
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN;
} else {
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
}
}
public boolean hasDistinctMultitouch() {
return mInputView != null && mInputView.hasDistinctMultitouch();
}
public void setAutoModeSwitchStateMomentary() {
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_MOMENTARY;
}
public boolean isInMomentaryAutoModeSwitchState() {
return mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_MOMENTARY;
}
public boolean isInChordingAutoModeSwitchState() {
return mAutoModeSwitchState == AUTO_MODE_SWITCH_STATE_CHORDING;
}
public boolean isVibrateAndSoundFeedbackRequired() {
return mInputView != null && !mInputView.isInSlidingKeyInput();
}
private int getPointerCount() {
return mInputView == null ? 0 : mInputView.getPointerCount();
}
/**
* Updates state machine to figure out when to automatically snap back to the previous mode.
*/
public void onKey(int key) {
// Switch back to alpha mode if user types one or more non-space/enter characters
// followed by a space/enter
switch (mAutoModeSwitchState) {
case AUTO_MODE_SWITCH_STATE_MOMENTARY:
// Only distinct multi touch devices can be in this state.
// On non-distinct multi touch devices, mode change key is handled by {@link onKey},
// not by {@link onPress} and {@link onRelease}. So, on such devices,
// {@link mAutoModeSwitchState} starts from {@link AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN},
// or {@link AUTO_MODE_SWITCH_STATE_ALPHA}, not from
// {@link AUTO_MODE_SWITCH_STATE_MOMENTARY}.
if (key == LatinKeyboard.KEYCODE_MODE_CHANGE) {
// Detected only the mode change key has been pressed, and then released.
if (mIsSymbols) {
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN;
} else {
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_ALPHA;
}
} else if (getPointerCount() == 1) {
// Snap back to the previous keyboard mode if the user pressed the mode change key
// and slid to other key, then released the finger.
// If the user cancels the sliding input, snapping back to the previous keyboard
// mode is handled by {@link #onCancelInput}.
mInputMethodService.changeKeyboardMode();
} else {
// Chording input is being started. The keyboard mode will be snapped back to the
// previous mode in {@link onReleaseSymbol} when the mode change key is released.
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_CHORDING;
}
break;
case AUTO_MODE_SWITCH_STATE_SYMBOL_BEGIN:
if (key != KP2AKeyboard.KEYCODE_SPACE && key != KP2AKeyboard.KEYCODE_ENTER && key >= 0) {
mAutoModeSwitchState = AUTO_MODE_SWITCH_STATE_SYMBOL;
}
break;
case AUTO_MODE_SWITCH_STATE_SYMBOL:
// Snap back to alpha keyboard mode if user types one or more non-space/enter
// characters followed by a space/enter.
if (key == KP2AKeyboard.KEYCODE_ENTER || key == KP2AKeyboard.KEYCODE_SPACE) {
mInputMethodService.changeKeyboardMode();
}
break;
}
}
public LatinKeyboardView getInputView() {
return mInputView;
}
public void recreateInputView() {
changeLatinKeyboardView(mLayoutId, true);
}
private void changeLatinKeyboardView(int newLayout, boolean forceReset) {
if (mLayoutId != newLayout || mInputView == null || forceReset) {
if (mInputView != null) {
mInputView.closing();
}
if (THEMES.length <= newLayout) {
newLayout = Integer.valueOf(DEFAULT_LAYOUT_ID);
}
LatinIMEUtil.GCUtils.getInstance().reset();
boolean tryGC = true;
for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
try {
mInputView = (LatinKeyboardView) mInputMethodService.getLayoutInflater(
).inflate(THEMES[newLayout], null);
tryGC = false;
} catch (OutOfMemoryError e) {
tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(
mLayoutId + "," + newLayout, e);
} catch (InflateException e) {
tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(
mLayoutId + "," + newLayout, e);
}
}
mInputView.setOnKeyboardActionListener(mInputMethodService);
mLayoutId = newLayout;
}
mInputMethodService.mHandler.post(new Runnable() {
public void run() {
if (mInputView != null) {
mInputMethodService.setInputView(mInputView);
}
mInputMethodService.updateInputViewShown();
}});
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PREF_KEYBOARD_LAYOUT.equals(key)) {
changeLatinKeyboardView(
Integer.valueOf(sharedPreferences.getString(key, DEFAULT_LAYOUT_ID)), false);
} else if (LatinIMESettings.PREF_SETTINGS_KEY.equals(key)) {
updateSettingsKeyState(sharedPreferences);
recreateInputView();
}
}
public boolean isBlackSym () {
if (mInputView != null && mInputView.getSymbolColorScheme() == 1) {
return true;
}
return false;
}
private int getCharColorId () {
if (isBlackSym()) {
return CHAR_THEME_COLOR_BLACK;
} else {
return CHAR_THEME_COLOR_WHITE;
}
}
public void onAutoCompletionStateChanged(boolean isAutoCompletion) {
if (isAutoCompletion != mIsAutoCompletionActive) {
LatinKeyboardView keyboardView = getInputView();
mIsAutoCompletionActive = isAutoCompletion;
keyboardView.invalidateKey(((LatinKeyboard) keyboardView.getKeyboard())
.onAutoCompletionStateChanged(isAutoCompletion));
}
}
private void updateSettingsKeyState(SharedPreferences prefs) {
Resources resources = mInputMethodService.getResources();
final String settingsKeyMode = prefs.getString(LatinIMESettings.PREF_SETTINGS_KEY,
resources.getString(DEFAULT_SETTINGS_KEY_MODE));
// We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or
// 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system
if (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))
|| (settingsKeyMode.equals(resources.getString(SETTINGS_KEY_MODE_AUTO))
&& LatinIMEUtil.hasMultipleEnabledIMEs(mInputMethodService))) {
mHasSettingsKey = true;
} else {
mHasSettingsKey = false;
}
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import java.util.Locale;
/**
* Keeps track of list of selected input languages and the current
* input language that the user has selected.
*/
public class LanguageSwitcher {
private Locale[] mLocales;
private KP2AKeyboard mIme;
private String[] mSelectedLanguageArray;
private String mSelectedLanguages;
private int mCurrentIndex = 0;
private String mDefaultInputLanguage;
private Locale mDefaultInputLocale;
private Locale mSystemLocale;
public LanguageSwitcher(KP2AKeyboard ime) {
mIme = ime;
mLocales = new Locale[0];
}
public Locale[] getLocales() {
return mLocales;
}
public int getLocaleCount() {
return mLocales.length;
}
/**
* Loads the currently selected input languages from shared preferences.
* @param sp
* @return whether there was any change
*/
public boolean loadLocales(SharedPreferences sp) {
String selectedLanguages = sp.getString(KP2AKeyboard.PREF_SELECTED_LANGUAGES, null);
String currentLanguage = sp.getString(KP2AKeyboard.PREF_INPUT_LANGUAGE, null);
if (selectedLanguages == null || selectedLanguages.length() < 1) {
loadDefaults();
if (mLocales.length == 0) {
return false;
}
mLocales = new Locale[0];
return true;
}
if (selectedLanguages.equals(mSelectedLanguages)) {
return false;
}
mSelectedLanguageArray = selectedLanguages.split(",");
mSelectedLanguages = selectedLanguages; // Cache it for comparison later
constructLocales();
mCurrentIndex = 0;
if (currentLanguage != null) {
// Find the index
mCurrentIndex = 0;
for (int i = 0; i < mLocales.length; i++) {
if (mSelectedLanguageArray[i].equals(currentLanguage)) {
mCurrentIndex = i;
break;
}
}
// If we didn't find the index, use the first one
}
return true;
}
private void loadDefaults() {
mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
String country = mDefaultInputLocale.getCountry();
mDefaultInputLanguage = mDefaultInputLocale.getLanguage() +
(TextUtils.isEmpty(country) ? "" : "_" + country);
}
private void constructLocales() {
mLocales = new Locale[mSelectedLanguageArray.length];
for (int i = 0; i < mLocales.length; i++) {
final String lang = mSelectedLanguageArray[i];
mLocales[i] = new Locale(lang.substring(0, 2),
lang.length() > 4 ? lang.substring(3, 5) : "");
}
}
/**
* Returns the currently selected input language code, or the display language code if
* no specific locale was selected for input.
*/
public String getInputLanguage() {
if (getLocaleCount() == 0) return mDefaultInputLanguage;
return mSelectedLanguageArray[mCurrentIndex];
}
/**
* Returns the list of enabled language codes.
*/
public String[] getEnabledLanguages() {
return mSelectedLanguageArray;
}
/**
* Returns the currently selected input locale, or the display locale if no specific
* locale was selected for input.
* @return
*/
public Locale getInputLocale() {
if (getLocaleCount() == 0) return mDefaultInputLocale;
return mLocales[mCurrentIndex];
}
/**
* Returns the next input locale in the list. Wraps around to the beginning of the
* list if we're at the end of the list.
* @return
*/
public Locale getNextInputLocale() {
if (getLocaleCount() == 0) return mDefaultInputLocale;
return mLocales[(mCurrentIndex + 1) % mLocales.length];
}
/**
* Sets the system locale (display UI) used for comparing with the input language.
* @param locale the locale of the system
*/
public void setSystemLocale(Locale locale) {
mSystemLocale = locale;
}
/**
* Returns the system locale.
* @return the system locale
*/
public Locale getSystemLocale() {
return mSystemLocale;
}
/**
* Returns the previous input locale in the list. Wraps around to the end of the
* list if we're at the beginning of the list.
* @return
*/
public Locale getPrevInputLocale() {
if (getLocaleCount() == 0) return mDefaultInputLocale;
return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length];
}
public void reset() {
mCurrentIndex = 0;
}
public void next() {
mCurrentIndex++;
if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around
}
public void prev() {
mCurrentIndex--;
if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around
}
public void persist() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme);
Editor editor = sp.edit();
editor.putString(KP2AKeyboard.PREF_INPUT_LANGUAGE, getInputLanguage());
SharedPreferencesCompat.apply(editor);
}
static String toTitleCase(String s, Locale locale) {
if (s.length() == 0) {
return s;
}
return s.toUpperCase(locale).charAt(0) + s.substring(1);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.app.backup.BackupAgentHelper;
import android.app.backup.SharedPreferencesBackupHelper;
/**
* Backs up the Latin IME shared preferences.
*/
public class LatinIMEBackupAgent extends BackupAgentHelper {
@Override
public void onCreate() {
addHelper("shared_pref", new SharedPreferencesBackupHelper(this,
getPackageName() + "_preferences"));
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (C) 2010 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 keepass2android.softkeyboard;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.PreferenceActivity;
import android.util.Log;
public class LatinIMEDebugSettings extends PreferenceActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = "LatinIMEDebugSettings";
private static final String DEBUG_MODE_KEY = "debug_mode";
private CheckBoxPreference mDebugMode;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_for_debug);
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
prefs.registerOnSharedPreferenceChangeListener(this);
mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY);
updateDebugMode();
}
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (key.equals(DEBUG_MODE_KEY)) {
if (mDebugMode != null) {
mDebugMode.setChecked(prefs.getBoolean(DEBUG_MODE_KEY, false));
updateDebugMode();
}
}
}
private void updateDebugMode() {
if (mDebugMode == null) {
return;
}
boolean isDebugMode = mDebugMode.isChecked();
String version = "";
try {
PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0);
version = "Version " + info.versionName;
} catch (NameNotFoundException e) {
Log.e(TAG, "Could not find version info.");
}
if (!isDebugMode) {
mDebugMode.setTitle(version);
mDebugMode.setSummary("");
} else {
mDebugMode.setTitle(getResources().getString(R.string.prefs_debug_mode));
mDebugMode.setSummary(version);
}
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (C) 2008 The Android Open Source Project
* Copyright (C) 2014 Philipp Crocoll <crocoapps@googlemail.com>
* Copyright (C) 2014 Wiktor Lawski <wiktor.lawski@gmail.com>
*
* 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 keepass2android.softkeyboard;
import java.util.ArrayList;
import java.util.Locale;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.backup.BackupManager;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.speech.SpeechRecognizer;
import android.text.AutoText;
import android.util.Log;
public class LatinIMESettings extends PreferenceActivity
implements SharedPreferences.OnSharedPreferenceChangeListener,
DialogInterface.OnDismissListener {
private static final String QUICK_FIXES_KEY = "quick_fixes";
private static final String PREDICTION_SETTINGS_KEY = "prediction_settings";
/* package */ static final String PREF_SETTINGS_KEY = "settings_key";
private static final String TAG = "LatinIMESettings";
private CheckBoxPreference mQuickFixes;
private ListPreference mSettingsKeyPreference;
private boolean mOkClicked = false;
@Override
protected void onCreate(Bundle icicle) {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(this);
Design.updateTheme(this, prefs);
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs);
mQuickFixes = (CheckBoxPreference) findPreference(QUICK_FIXES_KEY);
mSettingsKeyPreference = (ListPreference) findPreference(PREF_SETTINGS_KEY);
prefs.registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onResume() {
super.onResume();
int autoTextSize = AutoText.getSize(getListView());
if (autoTextSize < 1) {
((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY))
.removePreference(mQuickFixes);
}
}
@Override
protected void onDestroy() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
this);
super.onDestroy();
}
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
(new BackupManager(this)).dataChanged();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
default:
Log.e(TAG, "unknown dialog " + id);
return null;
}
}
public void onDismiss(DialogInterface dialog) {
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright (C) 2010 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 keepass2android.softkeyboard;
import android.view.inputmethod.InputMethodManager;
import android.content.Context;
import android.os.AsyncTask;
import android.text.format.DateUtils;
import android.util.Log;
public class LatinIMEUtil {
/**
* Cancel an {@link AsyncTask}.
*
* @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete.
*/
public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
task.cancel(mayInterruptIfRunning);
}
}
public static class GCUtils {
private static final String TAG = "GCUtils";
public static final int GC_TRY_COUNT = 2;
// GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
// GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
public static final int GC_TRY_LOOP_MAX = 5;
private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
private static GCUtils sInstance = new GCUtils();
private int mGCTryCount = 0;
public static GCUtils getInstance() {
return sInstance;
}
public void reset() {
mGCTryCount = 0;
}
public boolean tryGCOrWait(String metaData, Throwable t) {
if (mGCTryCount == 0) {
System.gc();
}
if (++mGCTryCount > GC_TRY_COUNT) {
LatinImeLogger.logOnException(metaData, t);
return false;
} else {
try {
Thread.sleep(GC_INTERVAL);
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Sleep was interrupted.");
LatinImeLogger.logOnException(metaData, t);
return false;
}
}
}
}
public static boolean hasMultipleEnabledIMEs(Context context) {
return ((InputMethodManager) context.getSystemService(
Context.INPUT_METHOD_SERVICE)).getEnabledInputMethodList().size() > 1;
}
/* package */ static class RingCharBuffer {
private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
private static final int INVALID_COORDINATE = -2;
/* package */ static final int BUFSIZE = 20;
private Context mContext;
private boolean mEnabled = false;
private int mEnd = 0;
/* package */ int mLength = 0;
private char[] mCharBuf = new char[BUFSIZE];
private int[] mXBuf = new int[BUFSIZE];
private int[] mYBuf = new int[BUFSIZE];
private RingCharBuffer() {
}
public static RingCharBuffer getInstance() {
return sRingCharBuffer;
}
public static RingCharBuffer init(Context context, boolean enabled) {
sRingCharBuffer.mContext = context;
sRingCharBuffer.mEnabled = enabled;
return sRingCharBuffer;
}
private int normalize(int in) {
int ret = in % BUFSIZE;
return ret < 0 ? ret + BUFSIZE : ret;
}
public void push(char c, int x, int y) {
if (!mEnabled) return;
mCharBuf[mEnd] = c;
mXBuf[mEnd] = x;
mYBuf[mEnd] = y;
mEnd = normalize(mEnd + 1);
if (mLength < BUFSIZE) {
++mLength;
}
}
public char pop() {
if (mLength < 1) {
return PLACEHOLDER_DELIMITER_CHAR;
} else {
mEnd = normalize(mEnd - 1);
--mLength;
return mCharBuf[mEnd];
}
}
public char getLastChar() {
if (mLength < 1) {
return PLACEHOLDER_DELIMITER_CHAR;
} else {
return mCharBuf[normalize(mEnd - 1)];
}
}
public int getPreviousX(char c, int back) {
int index = normalize(mEnd - 2 - back);
if (mLength <= back
|| Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
return INVALID_COORDINATE;
} else {
return mXBuf[index];
}
}
public int getPreviousY(char c, int back) {
int index = normalize(mEnd - 2 - back);
if (mLength <= back
|| Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
return INVALID_COORDINATE;
} else {
return mYBuf[index];
}
}
public String getLastString() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < mLength; ++i) {
char c = mCharBuf[normalize(mEnd - 1 - i)];
if (!((KP2AKeyboard)mContext).isWordSeparator(c)) {
sb.append(c);
} else {
break;
}
}
return sb.reverse().toString();
}
public void reset() {
mLength = 0;
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 2010 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 keepass2android.softkeyboard;
import keepass2android.softkeyboard.Dictionary.DataType;
import android.content.Context;
import android.content.SharedPreferences;
import android.inputmethodservice.Keyboard;
import java.util.List;
public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
}
public static void init(Context context) {
}
public static void commit() {
}
public static void onDestroy() {
}
public static void logOnManualSuggestion(
String before, String after, int position, List<CharSequence> suggestions) {
}
public static void logOnAutoSuggestion(String before, String after) {
}
public static void logOnAutoSuggestionCanceled() {
}
public static void logOnDelete() {
}
public static void logOnInputChar() {
}
public static void logOnException(String metaData, Throwable e) {
}
public static void logOnWarning(String warning) {
}
public static void onStartSuggestion(CharSequence previousWords) {
}
public static void onAddSuggestedWord(String word, int typeId, DataType dataType) {
}
public static void onSetKeyboard(Keyboard kb) {
}
}

View File

@ -0,0 +1,385 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.Keyboard.Key;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import java.util.List;
public class LatinKeyboardView extends LatinKeyboardBaseView {
static final int KEYCODE_OPTIONS = -100;
static final int KEYCODE_OPTIONS_LONGPRESS = -101;
static final int KEYCODE_F1 = -103;
static final int KEYCODE_NEXT_LANGUAGE = -104;
static final int KEYCODE_PREV_LANGUAGE = -105;
static final int KEYCODE_KP2A = -200;
static final int KEYCODE_KP2A_USER = -201;
static final int KEYCODE_KP2A_PASSWORD = -202;
static final int KEYCODE_KP2A_ALPHA = -203;
static final int KEYCODE_KP2A_SWITCH = -204;
static final int KEYCODE_KP2A_LOCK = -205;
private Keyboard mPhoneKeyboard;
/** Whether we've started dropping move events because we found a big jump */
private boolean mDroppingEvents;
/**
* Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has
* occured
*/
private boolean mDisableDisambiguation;
/** The distance threshold at which we start treating the touch session as a multi-touch */
private int mJumpThresholdSquare = Integer.MAX_VALUE;
/** The y coordinate of the last row */
private int mLastRowY;
public LatinKeyboardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setPhoneKeyboard(Keyboard phoneKeyboard) {
mPhoneKeyboard = phoneKeyboard;
}
@Override
public void setPreviewEnabled(boolean previewEnabled) {
if (getKeyboard() == mPhoneKeyboard) {
// Phone keyboard never shows popup preview (except language switch).
super.setPreviewEnabled(false);
} else {
super.setPreviewEnabled(previewEnabled);
}
}
@Override
public void setKeyboard(Keyboard newKeyboard) {
final Keyboard oldKeyboard = getKeyboard();
if (oldKeyboard instanceof LatinKeyboard) {
// Reset old keyboard state before switching to new keyboard.
((LatinKeyboard)oldKeyboard).keyReleased();
}
super.setKeyboard(newKeyboard);
// One-seventh of the keyboard width seems like a reasonable threshold
mJumpThresholdSquare = newKeyboard.getMinWidth() / 7;
mJumpThresholdSquare *= mJumpThresholdSquare;
// Assuming there are 4 rows, this is the coordinate of the last row
mLastRowY = (newKeyboard.getHeight() * 3) / 4;
setKeyboardLocal(newKeyboard);
}
@Override
protected boolean onLongPress(Key key) {
int primaryCode = key.codes[0];
if (primaryCode == KEYCODE_OPTIONS) {
return invokeOnKey(KEYCODE_OPTIONS_LONGPRESS);
} else if (primaryCode == '0' && getKeyboard() == mPhoneKeyboard) {
// Long pressing on 0 in phone number keypad gives you a '+'.
return invokeOnKey('+');
} else {
return super.onLongPress(key);
}
}
private boolean invokeOnKey(int primaryCode) {
getOnKeyboardActionListener().onKey(primaryCode, null,
LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE,
LatinKeyboardBaseView.NOT_A_TOUCH_COORDINATE);
return true;
}
@Override
protected CharSequence adjustCase(CharSequence label) {
Keyboard keyboard = getKeyboard();
if (keyboard.isShifted()
&& keyboard instanceof LatinKeyboard
&& ((LatinKeyboard) keyboard).isAlphaKeyboard()
&& !TextUtils.isEmpty(label) && label.length() < 3
&& Character.isLowerCase(label.charAt(0))) {
return label.toString().toUpperCase(getKeyboardLocale());
}
return label;
}
public boolean setShiftLocked(boolean shiftLocked) {
Keyboard keyboard = getKeyboard();
if (keyboard instanceof LatinKeyboard) {
((LatinKeyboard)keyboard).setShiftLocked(shiftLocked);
invalidateAllKeys();
return true;
}
return false;
}
/**
* This function checks to see if we need to handle any sudden jumps in the pointer location
* that could be due to a multi-touch being treated as a move by the firmware or hardware.
* Once a sudden jump is detected, all subsequent move events are discarded
* until an UP is received.<P>
* When a sudden jump is detected, an UP event is simulated at the last position and when
* the sudden moves subside, a DOWN event is simulated for the second key.
* @param me the motion event
* @return true if the event was consumed, so that it doesn't continue to be handled by
* KeyboardView.
*/
private boolean handleSuddenJump(MotionEvent me) {
final int action = me.getAction();
final int x = (int) me.getX();
final int y = (int) me.getY();
boolean result = false;
// Real multi-touch event? Stop looking for sudden jumps
if (me.getPointerCount() > 1) {
mDisableDisambiguation = true;
}
if (mDisableDisambiguation) {
// If UP, reset the multi-touch flag
if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false;
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
// Reset the "session"
mDroppingEvents = false;
mDisableDisambiguation = false;
break;
case MotionEvent.ACTION_MOVE:
// Is this a big jump?
final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y);
// Check the distance and also if the move is not entirely within the bottom row
// If it's only in the bottom row, it might be an intentional slide gesture
// for language switching
if (distanceSquare > mJumpThresholdSquare
&& (mLastY < mLastRowY || y < mLastRowY)) {
// If we're not yet dropping events, start dropping and send an UP event
if (!mDroppingEvents) {
mDroppingEvents = true;
// Send an up event
MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
MotionEvent.ACTION_UP,
mLastX, mLastY, me.getMetaState());
super.onTouchEvent(translated);
translated.recycle();
}
result = true;
} else if (mDroppingEvents) {
// If moves are small and we're already dropping events, continue dropping
result = true;
}
break;
case MotionEvent.ACTION_UP:
if (mDroppingEvents) {
// Send a down event first, as we dropped a bunch of sudden jumps and assume that
// the user is releasing the touch on the second key.
MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
MotionEvent.ACTION_DOWN,
x, y, me.getMetaState());
super.onTouchEvent(translated);
translated.recycle();
mDroppingEvents = false;
// Let the up event get processed as well, result = false
}
break;
}
// Track the previous coordinate
mLastX = x;
mLastY = y;
return result;
}
@Override
public boolean onTouchEvent(MotionEvent me) {
LatinKeyboard keyboard = (LatinKeyboard) getKeyboard();
if (DEBUG_LINE) {
mLastX = (int) me.getX();
mLastY = (int) me.getY();
invalidate();
}
// If there was a sudden jump, return without processing the actual motion event.
if (handleSuddenJump(me))
return true;
// Reset any bounding box controls in the keyboard
if (me.getAction() == MotionEvent.ACTION_DOWN) {
keyboard.keyReleased();
}
if (me.getAction() == MotionEvent.ACTION_UP) {
int languageDirection = keyboard.getLanguageChangeDirection();
if (languageDirection != 0) {
getOnKeyboardActionListener().onKey(
languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE,
null, mLastX, mLastY);
me.setAction(MotionEvent.ACTION_CANCEL);
keyboard.keyReleased();
return super.onTouchEvent(me);
}
}
return super.onTouchEvent(me);
}
/**************************** INSTRUMENTATION *******************************/
static final boolean DEBUG_AUTO_PLAY = false;
static final boolean DEBUG_LINE = false;
private static final int MSG_TOUCH_DOWN = 1;
private static final int MSG_TOUCH_UP = 2;
Handler mHandler2;
private String mStringToPlay;
private int mStringIndex;
private boolean mDownDelivered;
private Key[] mAsciiKeys = new Key[256];
private boolean mPlaying;
private int mLastX;
private int mLastY;
private Paint mPaint;
private void setKeyboardLocal(Keyboard k) {
if (DEBUG_AUTO_PLAY) {
findKeys();
if (mHandler2 == null) {
mHandler2 = new Handler() {
@Override
public void handleMessage(Message msg) {
removeMessages(MSG_TOUCH_DOWN);
removeMessages(MSG_TOUCH_UP);
if (mPlaying == false) return;
switch (msg.what) {
case MSG_TOUCH_DOWN:
if (mStringIndex >= mStringToPlay.length()) {
mPlaying = false;
return;
}
char c = mStringToPlay.charAt(mStringIndex);
while (c > 255 || mAsciiKeys[c] == null) {
mStringIndex++;
if (mStringIndex >= mStringToPlay.length()) {
mPlaying = false;
return;
}
c = mStringToPlay.charAt(mStringIndex);
}
int x = mAsciiKeys[c].x + 10;
int y = mAsciiKeys[c].y + 26;
MotionEvent me = MotionEvent.obtain(SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_DOWN, x, y, 0);
LatinKeyboardView.this.dispatchTouchEvent(me);
me.recycle();
sendEmptyMessageDelayed(MSG_TOUCH_UP, 500); // Deliver up in 500ms if nothing else
// happens
mDownDelivered = true;
break;
case MSG_TOUCH_UP:
char cUp = mStringToPlay.charAt(mStringIndex);
int x2 = mAsciiKeys[cUp].x + 10;
int y2 = mAsciiKeys[cUp].y + 26;
mStringIndex++;
MotionEvent me2 = MotionEvent.obtain(SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_UP, x2, y2, 0);
LatinKeyboardView.this.dispatchTouchEvent(me2);
me2.recycle();
sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 500); // Deliver up in 500ms if nothing else
// happens
mDownDelivered = false;
break;
}
}
};
}
}
}
private void findKeys() {
List<Key> keys = getKeyboard().getKeys();
// Get the keys on this keyboard
for (int i = 0; i < keys.size(); i++) {
int code = keys.get(i).codes[0];
if (code >= 0 && code <= 255) {
mAsciiKeys[code] = keys.get(i);
}
}
}
public void startPlaying(String s) {
if (DEBUG_AUTO_PLAY) {
if (s == null) return;
mStringToPlay = s.toLowerCase();
mPlaying = true;
mDownDelivered = false;
mStringIndex = 0;
mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10);
}
}
@Override
public void draw(Canvas c) {
LatinIMEUtil.GCUtils.getInstance().reset();
boolean tryGC = true;
for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
try {
super.draw(c);
tryGC = false;
} catch (OutOfMemoryError e) {
tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e);
}
}
if (DEBUG_AUTO_PLAY) {
if (mPlaying) {
mHandler2.removeMessages(MSG_TOUCH_DOWN);
mHandler2.removeMessages(MSG_TOUCH_UP);
if (mDownDelivered) {
mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20);
} else {
mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20);
}
}
}
if (DEBUG_LINE) {
if (mPaint == null) {
mPaint = new Paint();
mPaint.setColor(0x80FFFFFF);
mPaint.setAntiAlias(false);
}
c.drawLine(mLastX, 0, mLastX, getHeight(), mPaint);
c.drawLine(0, mLastY, getWidth(), mLastY, mPaint);
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.inputmethodservice.Keyboard.Key;
class MiniKeyboardKeyDetector extends KeyDetector {
private static final int MAX_NEARBY_KEYS = 1;
private final int mSlideAllowanceSquare;
private final int mSlideAllowanceSquareTop;
public MiniKeyboardKeyDetector(float slideAllowance) {
super();
mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance);
// Top slide allowance is slightly longer (sqrt(2) times) than other edges.
mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2;
}
@Override
protected int getMaxNearbyKeys() {
return MAX_NEARBY_KEYS;
}
@Override
public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) {
final Key[] keys = getKeys();
final int touchX = getTouchX(x);
final int touchY = getTouchY(y);
int closestKeyIndex = LatinKeyboardBaseView.NOT_A_KEY;
int closestKeyDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
final int keyCount = keys.length;
for (int i = 0; i < keyCount; i++) {
final Key key = keys[i];
int dist = key.squaredDistanceFrom(touchX, touchY);
if (dist < closestKeyDist) {
closestKeyIndex = i;
closestKeyDist = dist;
}
}
if (allKeys != null && closestKeyIndex != LatinKeyboardBaseView.NOT_A_KEY)
allKeys[0] = keys[closestKeyIndex].codes[0];
return closestKeyIndex;
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
class ModifierKeyState {
private static final int RELEASING = 0;
private static final int PRESSING = 1;
private static final int MOMENTARY = 2;
private int mState = RELEASING;
public void onPress() {
mState = PRESSING;
}
public void onRelease() {
mState = RELEASING;
}
public void onOtherKeyPressed() {
if (mState == PRESSING)
mState = MOMENTARY;
}
public boolean isMomentary() {
return mState == MOMENTARY;
}
}

View File

@ -0,0 +1,259 @@
package keepass2android.softkeyboard;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.xmlpull.v1.XmlPullParserException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.util.Log;
public class PluginManager extends BroadcastReceiver {
private static String TAG = "PCKeyboard";
private static String HK_INTENT_DICT = "org.pocketworkstation.DICT";
private static String SOFTKEYBOARD_INTENT_DICT = "com.menny.android.anysoftkeyboard.DICTIONARY";
private KP2AKeyboard mIME;
// Apparently anysoftkeyboard doesn't use ISO 639-1 language codes for its locales?
// Add exceptions as needed.
private static Map<String, String> SOFTKEYBOARD_LANG_MAP = new HashMap<String, String>();
static {
SOFTKEYBOARD_LANG_MAP.put("dk", "da");
}
PluginManager(KP2AKeyboard ime) {
super();
mIME = ime;
}
private static Map<String, DictPluginSpec> mPluginDicts =
new HashMap<String, DictPluginSpec>();
static interface DictPluginSpec {
BinaryDictionary getDict(Context context);
}
static private abstract class DictPluginSpecBase
implements DictPluginSpec {
String mPackageName;
Resources getResources(Context context) {
PackageManager packageManager = context.getPackageManager();
Resources res = null;
try {
ApplicationInfo appInfo = packageManager.getApplicationInfo(mPackageName, 0);
res = packageManager.getResourcesForApplication(appInfo);
} catch (NameNotFoundException e) {
Log.i(TAG, "couldn't get resources");
}
return res;
}
abstract InputStream[] getStreams(Resources res);
public BinaryDictionary getDict(Context context) {
Resources res = getResources(context);
if (res == null) return null;
InputStream[] dicts = getStreams(res);
if (dicts == null) return null;
BinaryDictionary dict = new BinaryDictionary(
context, dicts, Suggest.DIC_MAIN);
if (dict.getSize() == 0) return null;
//Log.i(TAG, "dict size=" + dict.getSize());
return dict;
}
}
static private class DictPluginSpecHK
extends DictPluginSpecBase {
int[] mRawIds;
public DictPluginSpecHK(String pkg, int[] ids) {
mPackageName = pkg;
mRawIds = ids;
}
@Override
InputStream[] getStreams(Resources res) {
if (mRawIds == null || mRawIds.length == 0) return null;
InputStream[] streams = new InputStream[mRawIds.length];
for (int i = 0; i < mRawIds.length; ++i) {
streams[i] = res.openRawResource(mRawIds[i]);
}
return streams;
}
}
static private class DictPluginSpecSoftKeyboard
extends DictPluginSpecBase {
String mAssetName;
public DictPluginSpecSoftKeyboard(String pkg, String asset) {
mPackageName = pkg;
mAssetName = asset;
}
@Override
InputStream[] getStreams(Resources res) {
if (mAssetName == null) return null;
try {
InputStream in = res.getAssets().open(mAssetName);
return new InputStream[] {in};
} catch (IOException e) {
Log.e(TAG, "Dictionary asset loading failure");
return null;
}
}
}
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Package information changed, updating dictionaries.");
getPluginDictionaries(context);
Log.i(TAG, "Finished updating dictionaries.");
mIME.toggleLanguage(true, true);
}
static void getSoftKeyboardDictionaries(PackageManager packageManager) {
Intent dictIntent = new Intent(SOFTKEYBOARD_INTENT_DICT);
List<ResolveInfo> dictPacks = packageManager.queryBroadcastReceivers(
dictIntent, PackageManager.GET_RECEIVERS);
for (ResolveInfo ri : dictPacks) {
ApplicationInfo appInfo = ri.activityInfo.applicationInfo;
String pkgName = appInfo.packageName;
boolean success = false;
try {
Resources res = packageManager.getResourcesForApplication(appInfo);
Log.i("KP2AK", "Found dictionary plugin package: " + pkgName);
int dictId = res.getIdentifier("dictionaries", "xml", pkgName);
if (dictId == 0) continue;
XmlResourceParser xrp = res.getXml(dictId);
String assetName = null;
String lang = null;
try {
int current = xrp.getEventType();
while (current != XmlResourceParser.END_DOCUMENT) {
if (current == XmlResourceParser.START_TAG) {
String tag = xrp.getName();
if (tag != null) {
if (tag.equals("Dictionary")) {
lang = xrp.getAttributeValue(null, "locale");
String convLang = SOFTKEYBOARD_LANG_MAP.get(lang);
if (convLang != null) lang = convLang;
String type = xrp.getAttributeValue(null, "type");
if (type == null || type.equals("raw") || type.equals("binary")) {
assetName = xrp.getAttributeValue(null, "dictionaryAssertName"); // sic
} else {
Log.w(TAG, "Unsupported AnySoftKeyboard dict type " + type);
}
//Log.i(TAG, "asset=" + assetName + " lang=" + lang);
}
}
}
xrp.next();
current = xrp.getEventType();
}
} catch (XmlPullParserException e) {
Log.e(TAG, "Dictionary XML parsing failure");
} catch (IOException e) {
Log.e(TAG, "Dictionary XML IOException");
}
if (assetName == null || lang == null) continue;
DictPluginSpec spec = new DictPluginSpecSoftKeyboard(pkgName, assetName);
mPluginDicts.put(lang, spec);
Log.i("KP2AK", "Found plugin dictionary: lang=" + lang + ", pkg=" + pkgName);
success = true;
} catch (NameNotFoundException e) {
Log.i("KP2AK", "bad");
} finally {
if (!success) {
Log.i("KP2AK", "failed to load plugin dictionary spec from " + pkgName);
}
}
}
}
static void getHKDictionaries(PackageManager packageManager) {
Intent dictIntent = new Intent(HK_INTENT_DICT);
List<ResolveInfo> dictPacks = packageManager.queryIntentActivities(dictIntent, 0);
for (ResolveInfo ri : dictPacks) {
ApplicationInfo appInfo = ri.activityInfo.applicationInfo;
String pkgName = appInfo.packageName;
boolean success = false;
try {
Resources res = packageManager.getResourcesForApplication(appInfo);
Log.i("KP2AK", "Found dictionary plugin package: " + pkgName);
int langId = res.getIdentifier("dict_language", "string", pkgName);
if (langId == 0) continue;
String lang = res.getString(langId);
int[] rawIds = null;
// Try single-file version first
int rawId = res.getIdentifier("main", "raw", pkgName);
if (rawId != 0) {
rawIds = new int[] { rawId };
} else {
// try multi-part version
int parts = 0;
List<Integer> ids = new ArrayList<Integer>();
while (true) {
int id = res.getIdentifier("main" + parts, "raw", pkgName);
if (id == 0) break;
ids.add(id);
++parts;
}
if (parts == 0) continue; // no parts found
rawIds = new int[parts];
for (int i = 0; i < parts; ++i) rawIds[i] = ids.get(i);
}
DictPluginSpec spec = new DictPluginSpecHK(pkgName, rawIds);
mPluginDicts.put(lang, spec);
Log.i("KP2AK", "Found plugin dictionary: lang=" + lang + ", pkg=" + pkgName);
success = true;
} catch (NameNotFoundException e) {
Log.i("KP2AK", "bad");
} finally {
if (!success) {
Log.i("KP2AK", "failed to load plugin dictionary spec from " + pkgName);
}
}
}
}
static void getPluginDictionaries(Context context) {
mPluginDicts.clear();
PackageManager packageManager = context.getPackageManager();
getSoftKeyboardDictionaries(packageManager);
getHKDictionaries(packageManager);
}
static BinaryDictionary getDictionary(Context context, String lang) {
Log.i("KP2AK", "Looking for plugin dictionary for lang=" + lang);
DictPluginSpec spec = mPluginDicts.get(lang);
if (spec == null) spec = mPluginDicts.get(lang.substring(0, 2));
if (spec == null) {
Log.i("KP2AK", "No plugin found.");
return null;
}
BinaryDictionary dict = spec.getDict(context);
Log.i("KP2AK", "Found plugin dictionary for " + lang + (dict == null ? " is null" : ", size=" + dict.getSize()));
return dict;
}
}

View File

@ -0,0 +1,581 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import keepass2android.softkeyboard.LatinKeyboardBaseView.OnKeyboardActionListener;
import keepass2android.softkeyboard.LatinKeyboardBaseView.UIHandler;
import android.content.res.Resources;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.Keyboard.Key;
import android.util.Log;
import android.view.MotionEvent;
public class PointerTracker {
private static final String TAG = "PointerTracker";
private static final boolean DEBUG = false;
private static final boolean DEBUG_MOVE = false;
public interface UIProxy {
public void invalidateKey(Key key);
public void showPreview(int keyIndex, PointerTracker tracker);
public boolean hasDistinctMultitouch();
}
public final int mPointerId;
// Timing constants
private final int mDelayBeforeKeyRepeatStart;
private final int mLongPressKeyTimeout;
private final int mMultiTapKeyTimeout;
// Miscellaneous constants
private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY;
private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
private final UIProxy mProxy;
private final UIHandler mHandler;
private final KeyDetector mKeyDetector;
private OnKeyboardActionListener mListener;
private final KeyboardSwitcher mKeyboardSwitcher;
private final boolean mHasDistinctMultitouch;
private Key[] mKeys;
private int mKeyHysteresisDistanceSquared = -1;
private final KeyState mKeyState;
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
// true if event is already translated to a key action (long press or mini-keyboard)
private boolean mKeyAlreadyProcessed;
// true if this pointer is repeatable key
private boolean mIsRepeatableKey;
// true if this pointer is in sliding key input
private boolean mIsInSlidingKeyInput;
// For multi-tap
private int mLastSentIndex;
private int mTapCount;
private long mLastTapTime;
private boolean mInMultiTap;
private final StringBuilder mPreviewLabel = new StringBuilder(1);
// pressed key
private int mPreviousKey = NOT_A_KEY;
// This class keeps track of a key index and a position where this pointer is.
private static class KeyState {
private final KeyDetector mKeyDetector;
// The position and time at which first down event occurred.
private int mStartX;
private int mStartY;
private long mDownTime;
// The current key index where this pointer is.
private int mKeyIndex = NOT_A_KEY;
// The position where mKeyIndex was recognized for the first time.
private int mKeyX;
private int mKeyY;
// Last pointer position.
private int mLastX;
private int mLastY;
public KeyState(KeyDetector keyDetecor) {
mKeyDetector = keyDetecor;
}
public int getKeyIndex() {
return mKeyIndex;
}
public int getKeyX() {
return mKeyX;
}
public int getKeyY() {
return mKeyY;
}
public int getStartX() {
return mStartX;
}
public int getStartY() {
return mStartY;
}
public long getDownTime() {
return mDownTime;
}
public int getLastX() {
return mLastX;
}
public int getLastY() {
return mLastY;
}
public int onDownKey(int x, int y, long eventTime) {
mStartX = x;
mStartY = y;
mDownTime = eventTime;
return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
}
private int onMoveKeyInternal(int x, int y) {
mLastX = x;
mLastY = y;
return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
}
public int onMoveKey(int x, int y) {
return onMoveKeyInternal(x, y);
}
public int onMoveToNewKey(int keyIndex, int x, int y) {
mKeyIndex = keyIndex;
mKeyX = x;
mKeyY = y;
return keyIndex;
}
public int onUpKey(int x, int y) {
return onMoveKeyInternal(x, y);
}
}
public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy,
Resources res) {
if (proxy == null || handler == null || keyDetector == null)
throw new NullPointerException();
mPointerId = id;
mProxy = proxy;
mHandler = handler;
mKeyDetector = keyDetector;
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
mKeyState = new KeyState(keyDetector);
mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout);
resetMultiTap();
}
public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
mListener = listener;
}
public void setKeyboard(Key[] keys, float keyHysteresisDistance) {
if (keys == null || keyHysteresisDistance < 0)
throw new IllegalArgumentException();
mKeys = keys;
mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
// Mark that keyboard layout has been changed.
mKeyboardLayoutHasBeenChanged = true;
}
public boolean isInSlidingKeyInput() {
return mIsInSlidingKeyInput;
}
private boolean isValidKeyIndex(int keyIndex) {
return keyIndex >= 0 && keyIndex < mKeys.length;
}
public Key getKey(int keyIndex) {
return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null;
}
private boolean isModifierInternal(int keyIndex) {
Key key = getKey(keyIndex);
if (key == null)
return false;
int primaryCode = key.codes[0];
return primaryCode == Keyboard.KEYCODE_SHIFT
|| primaryCode == Keyboard.KEYCODE_MODE_CHANGE;
}
public boolean isModifier() {
return isModifierInternal(mKeyState.getKeyIndex());
}
public boolean isOnModifierKey(int x, int y) {
return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
}
public boolean isSpaceKey(int keyIndex) {
Key key = getKey(keyIndex);
return key != null && key.codes[0] == KP2AKeyboard.KEYCODE_SPACE;
}
public void updateKey(int keyIndex) {
if (mKeyAlreadyProcessed)
return;
int oldKeyIndex = mPreviousKey;
mPreviousKey = keyIndex;
if (keyIndex != oldKeyIndex) {
if (isValidKeyIndex(oldKeyIndex)) {
// if new key index is not a key, old key was just released inside of the key.
final boolean inside = (keyIndex == NOT_A_KEY);
mKeys[oldKeyIndex].onReleased(inside);
mProxy.invalidateKey(mKeys[oldKeyIndex]);
}
if (isValidKeyIndex(keyIndex)) {
mKeys[keyIndex].onPressed();
mProxy.invalidateKey(mKeys[keyIndex]);
}
}
}
public void setAlreadyProcessed() {
mKeyAlreadyProcessed = true;
}
public void onTouchEvent(int action, int x, int y, long eventTime) {
switch (action) {
case MotionEvent.ACTION_MOVE:
onMoveEvent(x, y, eventTime);
break;
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
onDownEvent(x, y, eventTime);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
onUpEvent(x, y, eventTime);
break;
case MotionEvent.ACTION_CANCEL:
onCancelEvent(x, y, eventTime);
break;
}
}
public void onDownEvent(int x, int y, long eventTime) {
if (DEBUG)
debugLog("onDownEvent:", x, y);
int keyIndex = mKeyState.onDownKey(x, y, eventTime);
mKeyboardLayoutHasBeenChanged = false;
mKeyAlreadyProcessed = false;
mIsRepeatableKey = false;
mIsInSlidingKeyInput = false;
checkMultiTap(eventTime, keyIndex);
if (mListener != null) {
if (isValidKeyIndex(keyIndex)) {
mListener.onPress(mKeys[keyIndex].codes[0]);
// This onPress call may have changed keyboard layout. Those cases are detected at
// {@link #setKeyboard}. In those cases, we should update keyIndex according to the
// new keyboard layout.
if (mKeyboardLayoutHasBeenChanged) {
mKeyboardLayoutHasBeenChanged = false;
keyIndex = mKeyState.onDownKey(x, y, eventTime);
}
}
}
if (isValidKeyIndex(keyIndex)) {
if (mKeys[keyIndex].repeatable) {
repeatKey(keyIndex);
mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
mIsRepeatableKey = true;
}
startLongPressTimer(keyIndex);
}
showKeyPreviewAndUpdateKey(keyIndex);
}
public void onMoveEvent(int x, int y, long eventTime) {
if (DEBUG_MOVE)
debugLog("onMoveEvent:", x, y);
if (mKeyAlreadyProcessed)
return;
final KeyState keyState = mKeyState;
int keyIndex = keyState.onMoveKey(x, y);
final Key oldKey = getKey(keyState.getKeyIndex());
if (isValidKeyIndex(keyIndex)) {
if (oldKey == null) {
// The pointer has been slid in to the new key, but the finger was not on any keys.
// In this case, we must call onPress() to notify that the new key is being pressed.
if (mListener != null) {
mListener.onPress(getKey(keyIndex).codes[0]);
// This onPress call may have changed keyboard layout. Those cases are detected
// at {@link #setKeyboard}. In those cases, we should update keyIndex according
// to the new keyboard layout.
if (mKeyboardLayoutHasBeenChanged) {
mKeyboardLayoutHasBeenChanged = false;
keyIndex = keyState.onMoveKey(x, y);
}
}
keyState.onMoveToNewKey(keyIndex, x, y);
startLongPressTimer(keyIndex);
} else if (!isMinorMoveBounce(x, y, keyIndex)) {
// The pointer has been slid in to the new key from the previous key, we must call
// onRelease() first to notify that the previous key has been released, then call
// onPress() to notify that the new key is being pressed.
mIsInSlidingKeyInput = true;
if (mListener != null)
mListener.onRelease(oldKey.codes[0]);
resetMultiTap();
if (mListener != null) {
mListener.onPress(getKey(keyIndex).codes[0]);
// This onPress call may have changed keyboard layout. Those cases are detected
// at {@link #setKeyboard}. In those cases, we should update keyIndex according
// to the new keyboard layout.
if (mKeyboardLayoutHasBeenChanged) {
mKeyboardLayoutHasBeenChanged = false;
keyIndex = keyState.onMoveKey(x, y);
}
}
keyState.onMoveToNewKey(keyIndex, x, y);
startLongPressTimer(keyIndex);
}
} else {
if (oldKey != null && !isMinorMoveBounce(x, y, keyIndex)) {
// The pointer has been slid out from the previous key, we must call onRelease() to
// notify that the previous key has been released.
mIsInSlidingKeyInput = true;
if (mListener != null)
mListener.onRelease(oldKey.codes[0]);
resetMultiTap();
keyState.onMoveToNewKey(keyIndex, x ,y);
mHandler.cancelLongPressTimer();
}
}
showKeyPreviewAndUpdateKey(keyState.getKeyIndex());
}
public void onUpEvent(int x, int y, long eventTime) {
if (DEBUG)
debugLog("onUpEvent :", x, y);
mHandler.cancelKeyTimers();
mHandler.cancelPopupPreview();
showKeyPreviewAndUpdateKey(NOT_A_KEY);
mIsInSlidingKeyInput = false;
if (mKeyAlreadyProcessed)
return;
int keyIndex = mKeyState.onUpKey(x, y);
if (isMinorMoveBounce(x, y, keyIndex)) {
// Use previous fixed key index and coordinates.
keyIndex = mKeyState.getKeyIndex();
x = mKeyState.getKeyX();
y = mKeyState.getKeyY();
}
if (!mIsRepeatableKey) {
detectAndSendKey(keyIndex, x, y, eventTime);
}
if (isValidKeyIndex(keyIndex))
mProxy.invalidateKey(mKeys[keyIndex]);
}
public void onCancelEvent(int x, int y, long eventTime) {
if (DEBUG)
debugLog("onCancelEvt:", x, y);
mHandler.cancelKeyTimers();
mHandler.cancelPopupPreview();
showKeyPreviewAndUpdateKey(NOT_A_KEY);
mIsInSlidingKeyInput = false;
int keyIndex = mKeyState.getKeyIndex();
if (isValidKeyIndex(keyIndex))
mProxy.invalidateKey(mKeys[keyIndex]);
}
public void repeatKey(int keyIndex) {
Key key = getKey(keyIndex);
if (key != null) {
// While key is repeating, because there is no need to handle multi-tap key, we can
// pass -1 as eventTime argument.
detectAndSendKey(keyIndex, key.x, key.y, -1);
}
}
public int getLastX() {
return mKeyState.getLastX();
}
public int getLastY() {
return mKeyState.getLastY();
}
public long getDownTime() {
return mKeyState.getDownTime();
}
// These package scope methods are only for debugging purpose.
/* package */ int getStartX() {
return mKeyState.getStartX();
}
/* package */ int getStartY() {
return mKeyState.getStartY();
}
private boolean isMinorMoveBounce(int x, int y, int newKey) {
if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
throw new IllegalStateException("keyboard and/or hysteresis not set");
int curKey = mKeyState.getKeyIndex();
if (newKey == curKey) {
return true;
} else if (isValidKeyIndex(curKey)) {
return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared;
} else {
return false;
}
}
private static int getSquareDistanceToKeyEdge(int x, int y, Key key) {
final int left = key.x;
final int right = key.x + key.width;
final int top = key.y;
final int bottom = key.y + key.height;
final int edgeX = x < left ? left : (x > right ? right : x);
final int edgeY = y < top ? top : (y > bottom ? bottom : y);
final int dx = x - edgeX;
final int dy = y - edgeY;
return dx * dx + dy * dy;
}
private void showKeyPreviewAndUpdateKey(int keyIndex) {
updateKey(keyIndex);
// The modifier key, such as shift key, should not be shown as preview when multi-touch is
// supported. On the other hand, if multi-touch is not supported, the modifier key should
// be shown as preview.
if (mHasDistinctMultitouch && isModifier()) {
mProxy.showPreview(NOT_A_KEY, this);
} else {
mProxy.showPreview(keyIndex, this);
}
}
private void startLongPressTimer(int keyIndex) {
if (mKeyboardSwitcher.isInMomentaryAutoModeSwitchState()) {
// We use longer timeout for sliding finger input started from the symbols mode key.
mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
} else {
mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
}
}
private void detectAndSendKey(int index, int x, int y, long eventTime) {
final OnKeyboardActionListener listener = mListener;
final Key key = getKey(index);
if (key == null) {
if (listener != null)
listener.onCancel();
} else {
if (key.text != null) {
if (listener != null) {
listener.onText(key.text);
listener.onRelease(0); // dummy key code
}
} else {
int code = key.codes[0];
int[] codes = mKeyDetector.newCodeArray();
mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
// Multi-tap
if (mInMultiTap) {
if (mTapCount != -1) {
mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y);
} else {
mTapCount = 0;
}
code = key.codes[mTapCount];
}
/*
* Swap the first and second values in the codes array if the primary code is not
* the first value but the second value in the array. This happens when key
* debouncing is in effect.
*/
if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
codes[1] = codes[0];
codes[0] = code;
}
if (listener != null) {
listener.onKey(code, codes, x, y);
listener.onRelease(code);
}
}
mLastSentIndex = index;
mLastTapTime = eventTime;
}
}
/**
* Handle multi-tap keys by producing the key label for the current multi-tap state.
*/
public CharSequence getPreviewText(Key key) {
if (mInMultiTap) {
// Multi-tap
mPreviewLabel.setLength(0);
mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
return mPreviewLabel;
} else {
return key.label;
}
}
private void resetMultiTap() {
mLastSentIndex = NOT_A_KEY;
mTapCount = 0;
mLastTapTime = -1;
mInMultiTap = false;
}
private void checkMultiTap(long eventTime, int keyIndex) {
Key key = getKey(keyIndex);
if (key == null)
return;
final boolean isMultiTap =
(eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex);
if (key.codes.length > 1) {
mInMultiTap = true;
if (isMultiTap) {
mTapCount = (mTapCount + 1) % key.codes.length;
return;
} else {
mTapCount = -1;
return;
}
}
if (!isMultiTap) {
resetMultiTap();
}
}
private void debugLog(String title, int x, int y) {
int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
Key key = getKey(keyIndex);
final String code;
if (key == null) {
code = "----";
} else {
int primaryCode = key.codes[0];
code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode);
}
Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title,
(mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code,
(isModifier() ? "modifier" : "")));
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.inputmethodservice.Keyboard.Key;
import java.util.Arrays;
class ProximityKeyDetector extends KeyDetector {
private static final int MAX_NEARBY_KEYS = 12;
// working area
private int[] mDistances = new int[MAX_NEARBY_KEYS];
@Override
protected int getMaxNearbyKeys() {
return MAX_NEARBY_KEYS;
}
@Override
public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) {
final Key[] keys = getKeys();
final int touchX = getTouchX(x);
final int touchY = getTouchY(y);
int primaryIndex = LatinKeyboardBaseView.NOT_A_KEY;
int closestKey = LatinKeyboardBaseView.NOT_A_KEY;
int closestKeyDist = mProximityThresholdSquare + 1;
int[] distances = mDistances;
Arrays.fill(distances, Integer.MAX_VALUE);
int [] nearestKeyIndices = mKeyboard.getNearestKeys(touchX, touchY);
final int keyCount = nearestKeyIndices.length;
for (int i = 0; i < keyCount; i++) {
final Key key = keys[nearestKeyIndices[i]];
int dist = 0;
boolean isInside = key.isInside(touchX, touchY);
if (isInside) {
primaryIndex = nearestKeyIndices[i];
}
if (((mProximityCorrectOn
&& (dist = key.squaredDistanceFrom(touchX, touchY)) < mProximityThresholdSquare)
|| isInside)
&& key.codes[0] > 32) {
// Find insertion point
final int nCodes = key.codes.length;
if (dist < closestKeyDist) {
closestKeyDist = dist;
closestKey = nearestKeyIndices[i];
}
if (allKeys == null) continue;
for (int j = 0; j < distances.length; j++) {
if (distances[j] > dist) {
// Make space for nCodes codes
System.arraycopy(distances, j, distances, j + nCodes,
distances.length - j - nCodes);
System.arraycopy(allKeys, j, allKeys, j + nCodes,
allKeys.length - j - nCodes);
System.arraycopy(key.codes, 0, allKeys, j, nCodes);
Arrays.fill(distances, j, j + nCodes, dist);
break;
}
}
}
}
if (primaryIndex == LatinKeyboardBaseView.NOT_A_KEY) {
primaryIndex = closestKey;
}
return primaryIndex;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2010 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 keepass2android.softkeyboard;
import android.content.SharedPreferences;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Reflection utils to call SharedPreferences$Editor.apply when possible,
* falling back to commit when apply isn't available.
*/
public class SharedPreferencesCompat {
private static final Method sApplyMethod = findApplyMethod();
private static Method findApplyMethod() {
try {
return SharedPreferences.Editor.class.getMethod("apply");
} catch (NoSuchMethodException unused) {
// fall through
}
return null;
}
public static void apply(SharedPreferences.Editor editor) {
if (sApplyMethod != null) {
try {
sApplyMethod.invoke(editor);
return;
} catch (InvocationTargetException unused) {
// fall through
} catch (IllegalAccessException unused) {
// fall through
}
}
editor.commit();
}
}

View File

@ -0,0 +1,552 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.content.Context;
import android.text.AutoText;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/**
* This class loads a dictionary and provides a list of suggestions for a given sequence of
* characters. This includes corrections and completions.
* @hide pending API Council Approval
*/
public class Suggest implements Dictionary.WordCallback {
public static final int APPROX_MAX_WORD_LENGTH = 32;
public static final int CORRECTION_NONE = 0;
public static final int CORRECTION_BASIC = 1;
public static final int CORRECTION_FULL = 2;
public static final int CORRECTION_FULL_BIGRAM = 3;
/**
* Words that appear in both bigram and unigram data gets multiplier ranging from
* BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the frequency score from
* bigram data.
*/
public static final double BIGRAM_MULTIPLIER_MIN = 1.2;
public static final double BIGRAM_MULTIPLIER_MAX = 1.5;
/**
* Maximum possible bigram frequency. Will depend on how many bits are being used in data
* structure. Maximum bigram freqeuncy will get the BIGRAM_MULTIPLIER_MAX as the multiplier.
*/
public static final int MAXIMUM_BIGRAM_FREQUENCY = 127;
public static final int DIC_USER_TYPED = 0;
public static final int DIC_MAIN = 1;
public static final int DIC_USER = 2;
public static final int DIC_AUTO = 3;
public static final int DIC_CONTACTS = 4;
// If you add a type of dictionary, increment DIC_TYPE_LAST_ID
public static final int DIC_TYPE_LAST_ID = 4;
static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000;
private BinaryDictionary mMainDict;
/*
private Dictionary mUserDictionary;
private Dictionary mAutoDictionary;
private Dictionary mContactsDictionary;
private Dictionary mUserBigramDictionary;
*/
private int mPrefMaxSuggestions = 12;
private static final int PREF_MAX_BIGRAMS = 60;
private boolean mAutoTextEnabled;
private int[] mPriorities = new int[mPrefMaxSuggestions];
private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS];
// Handle predictive correction for only the first 1280 characters for performance reasons
// If we support scripts that need latin characters beyond that, we should probably use some
// kind of a sparse array or language specific list with a mapping lookup table.
// 1280 is the size of the BASE_CHARS array in ExpandableDictionary, which is a basic set of
// latin characters.
private int[] mNextLettersFrequencies = new int[1280];
private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>();
private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
private boolean mHaveCorrection;
private CharSequence mOriginalWord;
private String mLowerOriginalWord;
// TODO: Remove these member variables by passing more context to addWord() callback method
private boolean mIsFirstCharCapitalized;
private boolean mIsAllUpperCase;
private int mCorrectionMode = CORRECTION_BASIC;
public Suggest(Context context, int[] dictionaryResId) {
mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN);
Locale locale = context.getResources().getConfiguration().locale;
Log.d("KP2AK", "locale: " + locale.getISO3Language());
if (!hasMainDictionary()
|| (!"eng".equals(locale.getISO3Language())))
{
Log.d("KP2AK", "try get plug");
BinaryDictionary plug = PluginManager.getDictionary(context, locale.getLanguage());
if (plug != null) {
Log.d("KP2AK", "ok");
mMainDict.close();
mMainDict = plug;
}
}
initPool();
}
private void initPool() {
for (int i = 0; i < mPrefMaxSuggestions; i++) {
StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
mStringPool.add(sb);
}
}
public void setAutoTextEnabled(boolean enabled) {
mAutoTextEnabled = enabled;
}
public int getCorrectionMode() {
return mCorrectionMode;
}
public void setCorrectionMode(int mode) {
mCorrectionMode = mode;
}
public boolean hasMainDictionary() {
return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD;
}
public int getApproxMaxWordLength() {
return APPROX_MAX_WORD_LENGTH;
}
/*
*//**
* Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
* before the main dictionary, if set.
*//*
public void setUserDictionary(Dictionary userDictionary) {
mUserDictionary = userDictionary;
}
*//**
* Sets an optional contacts dictionary resource to be loaded.
*//*
public void setContactsDictionary(Dictionary userDictionary) {
mContactsDictionary = userDictionary;
}
public void setAutoDictionary(Dictionary autoDictionary) {
mAutoDictionary = autoDictionary;
}
public void setUserBigramDictionary(Dictionary userBigramDictionary) {
mUserBigramDictionary = userBigramDictionary;
}
*/
/**
* Number of suggestions to generate from the input key sequence. This has
* to be a number between 1 and 100 (inclusive).
* @param maxSuggestions
* @throws IllegalArgumentException if the number is out of range
*/
public void setMaxSuggestions(int maxSuggestions) {
if (maxSuggestions < 1 || maxSuggestions > 100) {
throw new IllegalArgumentException("maxSuggestions must be between 1 and 100");
}
mPrefMaxSuggestions = maxSuggestions;
mPriorities = new int[mPrefMaxSuggestions];
mBigramPriorities = new int[PREF_MAX_BIGRAMS];
collectGarbage(mSuggestions, mPrefMaxSuggestions);
while (mStringPool.size() < mPrefMaxSuggestions) {
StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
mStringPool.add(sb);
}
}
private boolean haveSufficientCommonality(String original, CharSequence suggestion) {
final int originalLength = original.length();
final int suggestionLength = suggestion.length();
final int minLength = Math.min(originalLength, suggestionLength);
if (minLength <= 2) return true;
int matching = 0;
int lessMatching = 0; // Count matches if we skip one character
int i;
for (i = 0; i < minLength; i++) {
final char origChar = ExpandableDictionary.toLowerCase(original.charAt(i));
if (origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i))) {
matching++;
lessMatching++;
} else if (i + 1 < suggestionLength
&& origChar == ExpandableDictionary.toLowerCase(suggestion.charAt(i + 1))) {
lessMatching++;
}
}
matching = Math.max(matching, lessMatching);
if (minLength <= 4) {
return matching >= 2;
} else {
return matching > minLength / 2;
}
}
/**
* Returns a list of words that match the list of character codes passed in.
* This list will be overwritten the next time this function is called.
* @param view a view for retrieving the context for AutoText
* @param wordComposer contains what is currently being typed
* @param prevWordForBigram previous word (used only for bigram)
* @return list of suggestions.
*/
public List<CharSequence> getSuggestions(View view, WordComposer wordComposer,
boolean includeTypedWordIfValid, CharSequence prevWordForBigram) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
mHaveCorrection = false;
mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
mIsAllUpperCase = wordComposer.isAllUpperCase();
collectGarbage(mSuggestions, mPrefMaxSuggestions);
Arrays.fill(mPriorities, 0);
Arrays.fill(mNextLettersFrequencies, 0);
// Save a lowercase version of the original word
mOriginalWord = wordComposer.getTypedWord();
if (mOriginalWord != null) {
final String mOriginalWordString = mOriginalWord.toString();
mOriginalWord = mOriginalWordString;
mLowerOriginalWord = mOriginalWordString.toLowerCase();
// Treating USER_TYPED as UNIGRAM suggestion for logging now.
LatinImeLogger.onAddSuggestedWord(mOriginalWordString, Suggest.DIC_USER_TYPED,
Dictionary.DataType.UNIGRAM);
} else {
mLowerOriginalWord = "";
}
if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
|| mCorrectionMode == CORRECTION_BASIC)) {
// At first character typed, search only the bigrams
Arrays.fill(mBigramPriorities, 0);
collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS);
if (!TextUtils.isEmpty(prevWordForBigram)) {
CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase();
if (mMainDict.isValidWord(lowerPrevWord)) {
prevWordForBigram = lowerPrevWord;
}
/*if (mUserBigramDictionary != null) {
mUserBigramDictionary.getBigrams(wordComposer, prevWordForBigram, this,
mNextLettersFrequencies);
}
if (mContactsDictionary != null) {
mContactsDictionary.getBigrams(wordComposer, prevWordForBigram, this,
mNextLettersFrequencies);
}*/
if (mMainDict != null) {
mMainDict.getBigrams(wordComposer, prevWordForBigram, this,
mNextLettersFrequencies);
}
char currentChar = wordComposer.getTypedWord().charAt(0);
// TODO: Must pay attention to locale when changing case.
char currentCharUpper = Character.toUpperCase(currentChar);
int count = 0;
int bigramSuggestionSize = mBigramSuggestions.size();
for (int i = 0; i < bigramSuggestionSize; i++) {
if (mBigramSuggestions.get(i).charAt(0) == currentChar
|| mBigramSuggestions.get(i).charAt(0) == currentCharUpper) {
int poolSize = mStringPool.size();
StringBuilder sb = poolSize > 0 ?
(StringBuilder) mStringPool.remove(poolSize - 1)
: new StringBuilder(getApproxMaxWordLength());
sb.setLength(0);
sb.append(mBigramSuggestions.get(i));
mSuggestions.add(count++, sb);
if (count > mPrefMaxSuggestions) break;
}
}
}
} else if (wordComposer.size() > 1) {
// At second character typed, search the unigrams (scores being affected by bigrams)
/*if (mUserDictionary != null || mContactsDictionary != null) {
if (mUserDictionary != null) {
mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
}
if (mContactsDictionary != null) {
mContactsDictionary.getWords(wordComposer, this, mNextLettersFrequencies);
}
if (mSuggestions.size() > 0 && isValidWord(mOriginalWord)
&& (mCorrectionMode == CORRECTION_FULL
|| mCorrectionMode == CORRECTION_FULL_BIGRAM)) {
mHaveCorrection = true;
}
}*/
mMainDict.getWords(wordComposer, this, mNextLettersFrequencies);
if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM)
&& mSuggestions.size() > 0) {
mHaveCorrection = true;
}
}
if (mOriginalWord != null) {
mSuggestions.add(0, mOriginalWord.toString());
}
// Check if the first suggestion has a minimum number of characters in common
if (wordComposer.size() > 1 && mSuggestions.size() > 1
&& (mCorrectionMode == CORRECTION_FULL
|| mCorrectionMode == CORRECTION_FULL_BIGRAM)) {
if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) {
mHaveCorrection = false;
}
}
if (mAutoTextEnabled) {
int i = 0;
int max = 6;
// Don't autotext the suggestions from the dictionaries
if (mCorrectionMode == CORRECTION_BASIC) max = 1;
while (i < mSuggestions.size() && i < max) {
String suggestedWord = mSuggestions.get(i).toString().toLowerCase();
CharSequence autoText =
AutoText.get(suggestedWord, 0, suggestedWord.length(), view);
// Is there an AutoText correction?
boolean canAdd = autoText != null;
// Is that correction already the current prediction (or original word)?
canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i));
// Is that correction already the next predicted word?
if (canAdd && i + 1 < mSuggestions.size() && mCorrectionMode != CORRECTION_BASIC) {
canAdd &= !TextUtils.equals(autoText, mSuggestions.get(i + 1));
}
if (canAdd) {
mHaveCorrection = true;
mSuggestions.add(i + 1, autoText);
i++;
}
i++;
}
}
removeDupes();
return mSuggestions;
}
public int[] getNextLettersFrequencies() {
return mNextLettersFrequencies;
}
private void removeDupes() {
final ArrayList<CharSequence> suggestions = mSuggestions;
if (suggestions.size() < 2) return;
int i = 1;
// Don't cache suggestions.size(), since we may be removing items
while (i < suggestions.size()) {
final CharSequence cur = suggestions.get(i);
// Compare each candidate with each previous candidate
for (int j = 0; j < i; j++) {
CharSequence previous = suggestions.get(j);
if (TextUtils.equals(cur, previous)) {
removeFromSuggestions(i);
i--;
break;
}
}
i++;
}
}
private void removeFromSuggestions(int index) {
CharSequence garbage = mSuggestions.remove(index);
if (garbage != null && garbage instanceof StringBuilder) {
mStringPool.add(garbage);
}
}
public boolean hasMinimalCorrection() {
return mHaveCorrection;
}
private boolean compareCaseInsensitive(final String mLowerOriginalWord,
final char[] word, final int offset, final int length) {
final int originalLength = mLowerOriginalWord.length();
if (originalLength == length && Character.isUpperCase(word[offset])) {
for (int i = 0; i < originalLength; i++) {
if (mLowerOriginalWord.charAt(i) != Character.toLowerCase(word[offset+i])) {
return false;
}
}
return true;
}
return false;
}
public boolean addWord(final char[] word, final int offset, final int length, int freq,
final int dicTypeId, final Dictionary.DataType dataType) {
Dictionary.DataType dataTypeForLog = dataType;
ArrayList<CharSequence> suggestions;
int[] priorities;
int prefMaxSuggestions;
if(dataType == Dictionary.DataType.BIGRAM) {
suggestions = mBigramSuggestions;
priorities = mBigramPriorities;
prefMaxSuggestions = PREF_MAX_BIGRAMS;
} else {
suggestions = mSuggestions;
priorities = mPriorities;
prefMaxSuggestions = mPrefMaxSuggestions;
}
int pos = 0;
// Check if it's the same word, only caps are different
if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) {
pos = 0;
} else {
if (dataType == Dictionary.DataType.UNIGRAM) {
// Check if the word was already added before (by bigram data)
int bigramSuggestion = searchBigramSuggestion(word,offset,length);
if(bigramSuggestion >= 0) {
dataTypeForLog = Dictionary.DataType.BIGRAM;
// turn freq from bigram into multiplier specified above
double multiplier = (((double) mBigramPriorities[bigramSuggestion])
/ MAXIMUM_BIGRAM_FREQUENCY)
* (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN)
+ BIGRAM_MULTIPLIER_MIN;
/* Log.d(TAG,"bigram num: " + bigramSuggestion
+ " wordB: " + mBigramSuggestions.get(bigramSuggestion).toString()
+ " currentPriority: " + freq + " bigramPriority: "
+ mBigramPriorities[bigramSuggestion]
+ " multiplier: " + multiplier); */
freq = (int)Math.round((freq * multiplier));
}
}
// Check the last one's priority and bail
if (priorities[prefMaxSuggestions - 1] >= freq) return true;
while (pos < prefMaxSuggestions) {
if (priorities[pos] < freq
|| (priorities[pos] == freq && length < suggestions.get(pos).length())) {
break;
}
pos++;
}
}
if (pos >= prefMaxSuggestions) {
return true;
}
System.arraycopy(priorities, pos, priorities, pos + 1,
prefMaxSuggestions - pos - 1);
priorities[pos] = freq;
int poolSize = mStringPool.size();
StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
: new StringBuilder(getApproxMaxWordLength());
sb.setLength(0);
// TODO: Must pay attention to locale when changing case.
if (mIsAllUpperCase) {
sb.append(new String(word, offset, length).toUpperCase());
} else if (mIsFirstCharCapitalized) {
sb.append(Character.toUpperCase(word[offset]));
if (length > 1) {
sb.append(word, offset + 1, length - 1);
}
} else {
sb.append(word, offset, length);
}
suggestions.add(pos, sb);
if (suggestions.size() > prefMaxSuggestions) {
CharSequence garbage = suggestions.remove(prefMaxSuggestions);
if (garbage instanceof StringBuilder) {
mStringPool.add(garbage);
}
} else {
LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
}
return true;
}
private int searchBigramSuggestion(final char[] word, final int offset, final int length) {
// TODO This is almost O(n^2). Might need fix.
// search whether the word appeared in bigram data
int bigramSuggestSize = mBigramSuggestions.size();
for(int i = 0; i < bigramSuggestSize; i++) {
if(mBigramSuggestions.get(i).length() == length) {
boolean chk = true;
for(int j = 0; j < length; j++) {
if(mBigramSuggestions.get(i).charAt(j) != word[offset+j]) {
chk = false;
break;
}
}
if(chk) return i;
}
}
return -1;
}
public boolean isValidWord(final CharSequence word) {
if (word == null || word.length() == 0) {
return false;
}
return mMainDict.isValidWord(word)
/*|| (mUserDictionary != null && mUserDictionary.isValidWord(word))
|| (mAutoDictionary != null && mAutoDictionary.isValidWord(word))
|| (mContactsDictionary != null && mContactsDictionary.isValidWord(word))*/;
}
private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) {
int poolSize = mStringPool.size();
int garbageSize = suggestions.size();
while (poolSize < prefMaxSuggestions && garbageSize > 0) {
CharSequence garbage = suggestions.get(garbageSize - 1);
if (garbage != null && garbage instanceof StringBuilder) {
mStringPool.add(garbage);
poolSize++;
}
garbageSize--;
}
if (poolSize == prefMaxSuggestions + 1) {
Log.w("Suggest", "String pool got too big: " + poolSize);
}
suggestions.clear();
}
public void close() {
if (mMainDict != null) {
mMainDict.close();
}
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.view.MotionEvent;
class SwipeTracker {
private static final int NUM_PAST = 4;
private static final int LONGEST_PAST_TIME = 200;
final EventRingBuffer mBuffer = new EventRingBuffer(NUM_PAST);
private float mYVelocity;
private float mXVelocity;
public void addMovement(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mBuffer.clear();
return;
}
long time = ev.getEventTime();
final int count = ev.getHistorySize();
for (int i = 0; i < count; i++) {
addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i), ev.getHistoricalEventTime(i));
}
addPoint(ev.getX(), ev.getY(), time);
}
private void addPoint(float x, float y, long time) {
final EventRingBuffer buffer = mBuffer;
while (buffer.size() > 0) {
long lastT = buffer.getTime(0);
if (lastT >= time - LONGEST_PAST_TIME)
break;
buffer.dropOldest();
}
buffer.add(x, y, time);
}
public void computeCurrentVelocity(int units) {
computeCurrentVelocity(units, Float.MAX_VALUE);
}
public void computeCurrentVelocity(int units, float maxVelocity) {
final EventRingBuffer buffer = mBuffer;
final float oldestX = buffer.getX(0);
final float oldestY = buffer.getY(0);
final long oldestTime = buffer.getTime(0);
float accumX = 0;
float accumY = 0;
final int count = buffer.size();
for (int pos = 1; pos < count; pos++) {
final int dur = (int)(buffer.getTime(pos) - oldestTime);
if (dur == 0) continue;
float dist = buffer.getX(pos) - oldestX;
float vel = (dist / dur) * units; // pixels/frame.
if (accumX == 0) accumX = vel;
else accumX = (accumX + vel) * .5f;
dist = buffer.getY(pos) - oldestY;
vel = (dist / dur) * units; // pixels/frame.
if (accumY == 0) accumY = vel;
else accumY = (accumY + vel) * .5f;
}
mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
: Math.min(accumX, maxVelocity);
mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
: Math.min(accumY, maxVelocity);
}
public float getXVelocity() {
return mXVelocity;
}
public float getYVelocity() {
return mYVelocity;
}
static class EventRingBuffer {
private final int bufSize;
private final float xBuf[];
private final float yBuf[];
private final long timeBuf[];
private int top; // points new event
private int end; // points oldest event
private int count; // the number of valid data
public EventRingBuffer(int max) {
this.bufSize = max;
xBuf = new float[max];
yBuf = new float[max];
timeBuf = new long[max];
clear();
}
public void clear() {
top = end = count = 0;
}
public int size() {
return count;
}
// Position 0 points oldest event
private int index(int pos) {
return (end + pos) % bufSize;
}
private int advance(int index) {
return (index + 1) % bufSize;
}
public void add(float x, float y, long time) {
xBuf[top] = x;
yBuf[top] = y;
timeBuf[top] = time;
top = advance(top);
if (count < bufSize) {
count++;
} else {
end = advance(end);
}
}
public float getX(int pos) {
return xBuf[index(pos)];
}
public float getY(int pos) {
return yBuf[index(pos)];
}
public long getTime(int pos) {
return timeBuf[index(pos)];
}
public void dropOldest() {
count--;
end = advance(end);
}
}
}

View File

@ -0,0 +1,278 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import android.content.Context;
import android.inputmethodservice.Keyboard.Key;
import android.text.format.DateFormat;
import android.util.Log;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Calendar;
public class TextEntryState {
private static final boolean DBG = false;
private static final String TAG = "TextEntryState";
private static boolean LOGGING = false;
private static int sBackspaceCount = 0;
private static int sAutoSuggestCount = 0;
private static int sAutoSuggestUndoneCount = 0;
private static int sManualSuggestCount = 0;
private static int sWordNotInDictionaryCount = 0;
private static int sSessionCount = 0;
private static int sTypedChars;
private static int sActualChars;
public enum State {
UNKNOWN,
START,
IN_WORD,
ACCEPTED_DEFAULT,
PICKED_SUGGESTION,
PUNCTUATION_AFTER_WORD,
PUNCTUATION_AFTER_ACCEPTED,
SPACE_AFTER_ACCEPTED,
SPACE_AFTER_PICKED,
UNDO_COMMIT,
CORRECTING,
PICKED_CORRECTION;
}
private static State sState = State.UNKNOWN;
private static FileOutputStream sKeyLocationFile;
private static FileOutputStream sUserActionFile;
public static void newSession(Context context) {
sSessionCount++;
sAutoSuggestCount = 0;
sBackspaceCount = 0;
sAutoSuggestUndoneCount = 0;
sManualSuggestCount = 0;
sWordNotInDictionaryCount = 0;
sTypedChars = 0;
sActualChars = 0;
sState = State.START;
if (LOGGING) {
try {
sKeyLocationFile = context.openFileOutput("key.txt", Context.MODE_APPEND);
sUserActionFile = context.openFileOutput("action.txt", Context.MODE_APPEND);
} catch (IOException ioe) {
Log.e("TextEntryState", "Couldn't open file for output: " + ioe);
}
}
}
public static void endSession() {
if (sKeyLocationFile == null) {
return;
}
try {
sKeyLocationFile.close();
// Write to log file
// Write timestamp, settings,
String out = DateFormat.format("MM:dd hh:mm:ss", Calendar.getInstance().getTime())
.toString()
+ " BS: " + sBackspaceCount
+ " auto: " + sAutoSuggestCount
+ " manual: " + sManualSuggestCount
+ " typed: " + sWordNotInDictionaryCount
+ " undone: " + sAutoSuggestUndoneCount
+ " saved: " + ((float) (sActualChars - sTypedChars) / sActualChars)
+ "\n";
sUserActionFile.write(out.getBytes());
sUserActionFile.close();
sKeyLocationFile = null;
sUserActionFile = null;
} catch (IOException ioe) {
}
}
public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) {
if (typedWord == null) return;
if (!typedWord.equals(actualWord)) {
sAutoSuggestCount++;
}
sTypedChars += typedWord.length();
sActualChars += actualWord.length();
sState = State.ACCEPTED_DEFAULT;
LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString());
displayState();
}
// State.ACCEPTED_DEFAULT will be changed to other sub-states
// (see "case ACCEPTED_DEFAULT" in typedCharacter() below),
// and should be restored back to State.ACCEPTED_DEFAULT after processing for each sub-state.
public static void backToAcceptedDefault(CharSequence typedWord) {
if (typedWord == null) return;
switch (sState) {
case SPACE_AFTER_ACCEPTED:
case PUNCTUATION_AFTER_ACCEPTED:
case IN_WORD:
sState = State.ACCEPTED_DEFAULT;
break;
}
displayState();
}
public static void acceptedTyped(CharSequence typedWord) {
sWordNotInDictionaryCount++;
sState = State.PICKED_SUGGESTION;
displayState();
}
public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) {
sManualSuggestCount++;
State oldState = sState;
if (typedWord.equals(actualWord)) {
acceptedTyped(typedWord);
}
if (oldState == State.CORRECTING || oldState == State.PICKED_CORRECTION) {
sState = State.PICKED_CORRECTION;
} else {
sState = State.PICKED_SUGGESTION;
}
displayState();
}
public static void selectedForCorrection() {
sState = State.CORRECTING;
displayState();
}
public static void typedCharacter(char c, boolean isSeparator) {
boolean isSpace = c == ' ';
switch (sState) {
case IN_WORD:
if (isSpace || isSeparator) {
sState = State.START;
} else {
// State hasn't changed.
}
break;
case ACCEPTED_DEFAULT:
case SPACE_AFTER_PICKED:
if (isSpace) {
sState = State.SPACE_AFTER_ACCEPTED;
} else if (isSeparator) {
sState = State.PUNCTUATION_AFTER_ACCEPTED;
} else {
sState = State.IN_WORD;
}
break;
case PICKED_SUGGESTION:
case PICKED_CORRECTION:
if (isSpace) {
sState = State.SPACE_AFTER_PICKED;
} else if (isSeparator) {
// Swap
sState = State.PUNCTUATION_AFTER_ACCEPTED;
} else {
sState = State.IN_WORD;
}
break;
case START:
case UNKNOWN:
case SPACE_AFTER_ACCEPTED:
case PUNCTUATION_AFTER_ACCEPTED:
case PUNCTUATION_AFTER_WORD:
if (!isSpace && !isSeparator) {
sState = State.IN_WORD;
} else {
sState = State.START;
}
break;
case UNDO_COMMIT:
if (isSpace || isSeparator) {
sState = State.ACCEPTED_DEFAULT;
} else {
sState = State.IN_WORD;
}
break;
case CORRECTING:
sState = State.START;
break;
}
displayState();
}
public static void backspace() {
if (sState == State.ACCEPTED_DEFAULT) {
sState = State.UNDO_COMMIT;
sAutoSuggestUndoneCount++;
LatinImeLogger.logOnAutoSuggestionCanceled();
} else if (sState == State.UNDO_COMMIT) {
sState = State.IN_WORD;
}
sBackspaceCount++;
displayState();
}
public static void reset() {
sState = State.START;
displayState();
}
public static State getState() {
if (DBG) {
Log.d(TAG, "Returning state = " + sState);
}
return sState;
}
public static boolean isCorrecting() {
return sState == State.CORRECTING || sState == State.PICKED_CORRECTION;
}
public static void keyPressedAt(Key key, int x, int y) {
if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) {
String out =
"KEY: " + (char) key.codes[0]
+ " X: " + x
+ " Y: " + y
+ " MX: " + (key.x + key.width / 2)
+ " MY: " + (key.y + key.height / 2)
+ "\n";
try {
sKeyLocationFile.write(out.getBytes());
} catch (IOException ioe) {
// TODO: May run out of space
}
}
}
private static void displayState() {
if (DBG) {
Log.d(TAG, "State = " + sState);
}
}
}

View File

@ -0,0 +1,401 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.util.Log;
/**
* Stores all the pairs user types in databases. Prune the database if the size
* gets too big. Unlike AutoDictionary, it even stores the pairs that are already
* in the dictionary.
*/
public class UserBigramDictionary extends ExpandableDictionary {
private static final String TAG = "UserBigramDictionary";
/** Any pair being typed or picked */
private static final int FREQUENCY_FOR_TYPED = 2;
/** Maximum frequency for all pairs */
private static final int FREQUENCY_MAX = 127;
/**
* If this pair is typed 6 times, it would be suggested.
* Should be smaller than ContactsDictionary.FREQUENCY_FOR_CONTACTS_BIGRAM
*/
protected static final int SUGGEST_THRESHOLD = 6 * FREQUENCY_FOR_TYPED;
/** Maximum number of pairs. Pruning will start when databases goes above this number. */
private static int sMaxUserBigrams = 10000;
/**
* When it hits maximum bigram pair, it will delete until you are left with
* only (sMaxUserBigrams - sDeleteUserBigrams) pairs.
* Do not keep this number small to avoid deleting too often.
*/
private static int sDeleteUserBigrams = 1000;
/**
* Database version should increase if the database structure changes
*/
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "userbigram_dict.db";
/** Name of the words table in the database */
private static final String MAIN_TABLE_NAME = "main";
// TODO: Consume less space by using a unique id for locale instead of the whole
// 2-5 character string. (Same TODO from AutoDictionary)
private static final String MAIN_COLUMN_ID = BaseColumns._ID;
private static final String MAIN_COLUMN_WORD1 = "word1";
private static final String MAIN_COLUMN_WORD2 = "word2";
private static final String MAIN_COLUMN_LOCALE = "locale";
/** Name of the frequency table in the database */
private static final String FREQ_TABLE_NAME = "frequency";
private static final String FREQ_COLUMN_ID = BaseColumns._ID;
private static final String FREQ_COLUMN_PAIR_ID = "pair_id";
private static final String FREQ_COLUMN_FREQUENCY = "freq";
private final KP2AKeyboard mIme;
/** Locale for which this auto dictionary is storing words */
private String mLocale;
private HashSet<Bigram> mPendingWrites = new HashSet<Bigram>();
private final Object mPendingWritesLock = new Object();
private static volatile boolean sUpdatingDB = false;
private final static HashMap<String, String> sDictProjectionMap;
static {
sDictProjectionMap = new HashMap<String, String>();
sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID);
sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1);
sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2);
sDictProjectionMap.put(MAIN_COLUMN_LOCALE, MAIN_COLUMN_LOCALE);
sDictProjectionMap.put(FREQ_COLUMN_ID, FREQ_COLUMN_ID);
sDictProjectionMap.put(FREQ_COLUMN_PAIR_ID, FREQ_COLUMN_PAIR_ID);
sDictProjectionMap.put(FREQ_COLUMN_FREQUENCY, FREQ_COLUMN_FREQUENCY);
}
private static DatabaseHelper sOpenHelper = null;
private static class Bigram {
String word1;
String word2;
int frequency;
Bigram(String word1, String word2, int frequency) {
this.word1 = word1;
this.word2 = word2;
this.frequency = frequency;
}
@Override
public boolean equals(Object bigram) {
Bigram bigram2 = (Bigram) bigram;
return (word1.equals(bigram2.word1) && word2.equals(bigram2.word2));
}
@Override
public int hashCode() {
return (word1 + " " + word2).hashCode();
}
}
public void setDatabaseMax(int maxUserBigram) {
sMaxUserBigrams = maxUserBigram;
}
public void setDatabaseDelete(int deleteUserBigram) {
sDeleteUserBigrams = deleteUserBigram;
}
public UserBigramDictionary(Context context, KP2AKeyboard ime, String locale, int dicTypeId) {
super(context, dicTypeId);
mIme = ime;
mLocale = locale;
if (sOpenHelper == null) {
sOpenHelper = new DatabaseHelper(getContext());
}
if (mLocale != null && mLocale.length() > 1) {
loadDictionary();
}
}
@Override
public void close() {
flushPendingWrites();
// Don't close the database as locale changes will require it to be reopened anyway
// Also, the database is written to somewhat frequently, so it needs to be kept alive
// throughout the life of the process.
// mOpenHelper.close();
super.close();
}
/**
* Pair will be added to the userbigram database.
*/
public int addBigrams(String word1, String word2) {
// remove caps
if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) {
word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1);
}
int freq = super.addBigram(word1, word2, FREQUENCY_FOR_TYPED);
if (freq > FREQUENCY_MAX) freq = FREQUENCY_MAX;
synchronized (mPendingWritesLock) {
if (freq == FREQUENCY_FOR_TYPED || mPendingWrites.isEmpty()) {
mPendingWrites.add(new Bigram(word1, word2, freq));
} else {
Bigram bi = new Bigram(word1, word2, freq);
mPendingWrites.remove(bi);
mPendingWrites.add(bi);
}
}
return freq;
}
/**
* Schedules a background thread to write any pending words to the database.
*/
public void flushPendingWrites() {
synchronized (mPendingWritesLock) {
// Nothing pending? Return
if (mPendingWrites.isEmpty()) return;
// Create a background thread to write the pending entries
new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute();
// Create a new map for writing new entries into while the old one is written to db
mPendingWrites = new HashSet<Bigram>();
}
}
/** Used for testing purpose **/
void waitUntilUpdateDBDone() {
synchronized (mPendingWritesLock) {
while (sUpdatingDB) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
return;
}
}
@Override
public void loadDictionaryAsync() {
// Load the words that correspond to the current input locale
Cursor cursor = query(MAIN_COLUMN_LOCALE + "=?", new String[] { mLocale });
try {
if (cursor.moveToFirst()) {
int word1Index = cursor.getColumnIndex(MAIN_COLUMN_WORD1);
int word2Index = cursor.getColumnIndex(MAIN_COLUMN_WORD2);
int frequencyIndex = cursor.getColumnIndex(FREQ_COLUMN_FREQUENCY);
while (!cursor.isAfterLast()) {
String word1 = cursor.getString(word1Index);
String word2 = cursor.getString(word2Index);
int frequency = cursor.getInt(frequencyIndex);
// Safeguard against adding really long words. Stack may overflow due
// to recursive lookup
if (word1.length() < MAX_WORD_LENGTH && word2.length() < MAX_WORD_LENGTH) {
super.setBigram(word1, word2, frequency);
}
cursor.moveToNext();
}
}
} finally {
cursor.close();
}
}
/**
* Query the database
*/
private Cursor query(String selection, String[] selectionArgs) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
// main INNER JOIN frequency ON (main._id=freq.pair_id)
qb.setTables(MAIN_TABLE_NAME + " INNER JOIN " + FREQ_TABLE_NAME + " ON ("
+ MAIN_TABLE_NAME + "." + MAIN_COLUMN_ID + "=" + FREQ_TABLE_NAME + "."
+ FREQ_COLUMN_PAIR_ID +")");
qb.setProjectionMap(sDictProjectionMap);
// Get the database and run the query
SQLiteDatabase db = sOpenHelper.getReadableDatabase();
Cursor c = qb.query(db,
new String[] { MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD2, FREQ_COLUMN_FREQUENCY },
selection, selectionArgs, null, null, null);
return c;
}
/**
* This class helps open, create, and upgrade the database file.
*/
private static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("PRAGMA foreign_keys = ON;");
db.execSQL("CREATE TABLE " + MAIN_TABLE_NAME + " ("
+ MAIN_COLUMN_ID + " INTEGER PRIMARY KEY,"
+ MAIN_COLUMN_WORD1 + " TEXT,"
+ MAIN_COLUMN_WORD2 + " TEXT,"
+ MAIN_COLUMN_LOCALE + " TEXT"
+ ");");
db.execSQL("CREATE TABLE " + FREQ_TABLE_NAME + " ("
+ FREQ_COLUMN_ID + " INTEGER PRIMARY KEY,"
+ FREQ_COLUMN_PAIR_ID + " INTEGER,"
+ FREQ_COLUMN_FREQUENCY + " INTEGER,"
+ "FOREIGN KEY(" + FREQ_COLUMN_PAIR_ID + ") REFERENCES " + MAIN_TABLE_NAME
+ "(" + MAIN_COLUMN_ID + ")" + " ON DELETE CASCADE"
+ ");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + MAIN_TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + FREQ_TABLE_NAME);
onCreate(db);
}
}
/**
* Async task to write pending words to the database so that it stays in sync with
* the in-memory trie.
*/
private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
private final HashSet<Bigram> mMap;
private final DatabaseHelper mDbHelper;
private final String mLocale;
public UpdateDbTask(Context context, DatabaseHelper openHelper,
HashSet<Bigram> pendingWrites, String locale) {
mMap = pendingWrites;
mLocale = locale;
mDbHelper = openHelper;
}
/** Prune any old data if the database is getting too big. */
private void checkPruneData(SQLiteDatabase db) {
db.execSQL("PRAGMA foreign_keys = ON;");
Cursor c = db.query(FREQ_TABLE_NAME, new String[] { FREQ_COLUMN_PAIR_ID },
null, null, null, null, null);
try {
int totalRowCount = c.getCount();
// prune out old data if we have too much data
if (totalRowCount > sMaxUserBigrams) {
int numDeleteRows = (totalRowCount - sMaxUserBigrams) + sDeleteUserBigrams;
int pairIdColumnId = c.getColumnIndex(FREQ_COLUMN_PAIR_ID);
c.moveToFirst();
int count = 0;
while (count < numDeleteRows && !c.isAfterLast()) {
String pairId = c.getString(pairIdColumnId);
// Deleting from MAIN table will delete the frequencies
// due to FOREIGN KEY .. ON DELETE CASCADE
db.delete(MAIN_TABLE_NAME, MAIN_COLUMN_ID + "=?",
new String[] { pairId });
c.moveToNext();
count++;
}
}
} finally {
c.close();
}
}
@Override
protected void onPreExecute() {
sUpdatingDB = true;
}
@Override
protected Void doInBackground(Void... v) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.execSQL("PRAGMA foreign_keys = ON;");
// Write all the entries to the db
Iterator<Bigram> iterator = mMap.iterator();
while (iterator.hasNext()) {
Bigram bi = iterator.next();
// find pair id
Cursor c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND "
+ MAIN_COLUMN_LOCALE + "=?",
new String[] { bi.word1, bi.word2, mLocale }, null, null, null);
int pairId;
if (c.moveToFirst()) {
// existing pair
pairId = c.getInt(c.getColumnIndex(MAIN_COLUMN_ID));
db.delete(FREQ_TABLE_NAME, FREQ_COLUMN_PAIR_ID + "=?",
new String[] { Integer.toString(pairId) });
} else {
// new pair
Long pairIdLong = db.insert(MAIN_TABLE_NAME, null,
getContentValues(bi.word1, bi.word2, mLocale));
pairId = pairIdLong.intValue();
}
c.close();
// insert new frequency
db.insert(FREQ_TABLE_NAME, null, getFrequencyContentValues(pairId, bi.frequency));
}
checkPruneData(db);
sUpdatingDB = false;
return null;
}
private ContentValues getContentValues(String word1, String word2, String locale) {
ContentValues values = new ContentValues(3);
values.put(MAIN_COLUMN_WORD1, word1);
values.put(MAIN_COLUMN_WORD2, word2);
values.put(MAIN_COLUMN_LOCALE, locale);
return values;
}
private ContentValues getFrequencyContentValues(int pairId, int frequency) {
ContentValues values = new ContentValues(2);
values.put(FREQ_COLUMN_PAIR_ID, pairId);
values.put(FREQ_COLUMN_FREQUENCY, frequency);
return values;
}
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package keepass2android.softkeyboard;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.provider.UserDictionary.Words;
public class UserDictionary extends ExpandableDictionary {
private static final String[] PROJECTION = {
Words._ID,
Words.WORD,
Words.FREQUENCY
};
private static final int INDEX_WORD = 1;
private static final int INDEX_FREQUENCY = 2;
private ContentObserver mObserver;
private String mLocale;
public UserDictionary(Context context, String locale) {
super(context, Suggest.DIC_USER);
mLocale = locale;
// Perform a managed query. The Activity will handle closing and requerying the cursor
// when needed.
ContentResolver cres = context.getContentResolver();
cres.registerContentObserver(Words.CONTENT_URI, true, mObserver = new ContentObserver(null) {
@Override
public void onChange(boolean self) {
setRequiresReload(true);
}
});
loadDictionary();
}
@Override
public synchronized void close() {
if (mObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mObserver);
mObserver = null;
}
super.close();
}
@Override
public void loadDictionaryAsync() {
//Cursor cursor = getContext().getContentResolver()
// .query(Words.CONTENT_URI, PROJECTION, "(locale IS NULL) or (locale=?)",
// new String[] { mLocale }, null);
//addWords(cursor);
}
/**
* Adds a word to the dictionary and makes it persistent.
* @param word the word to add. If the word is capitalized, then the dictionary will
* recognize it as a capitalized word when searched.
* @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
* the highest.
* @TODO use a higher or float range for frequency
*/
@Override
public synchronized void addWord(String word, int frequency) {
// Force load the dictionary here synchronously
if (getRequiresReload()) loadDictionaryAsync();
// Safeguard against adding long words. Can cause stack overflow.
if (word.length() >= getMaxWordLength()) return;
super.addWord(word, frequency);
// Update the user dictionary provider
final ContentValues values = new ContentValues(5);
values.put(Words.WORD, word);
values.put(Words.FREQUENCY, frequency);
values.put(Words.LOCALE, mLocale);
values.put(Words.APP_ID, 0);
final ContentResolver contentResolver = getContext().getContentResolver();
new Thread("addWord") {
public void run() {
contentResolver.insert(Words.CONTENT_URI, values);
}
}.start();
// In case the above does a synchronous callback of the change observer
setRequiresReload(false);
}
@Override
public synchronized void getWords(final WordComposer codes, final WordCallback callback,
int[] nextLettersFrequencies) {
super.getWords(codes, callback, nextLettersFrequencies);
}
@Override
public synchronized boolean isValidWord(CharSequence word) {
return super.isValidWord(word);
}
private void addWords(Cursor cursor) {
clearDictionary();
final int maxWordLength = getMaxWordLength();
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
String word = cursor.getString(INDEX_WORD);
int frequency = cursor.getInt(INDEX_FREQUENCY);
// Safeguard against adding really long words. Stack may overflow due
// to recursion
if (word.length() < maxWordLength) {
super.addWord(word, frequency);
}
cursor.moveToNext();
}
}
cursor.close();
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package keepass2android.softkeyboard;
import java.util.ArrayList;
/**
* A place to store the currently composing word with information such as adjacent key codes as well
*/
public class WordComposer {
/**
* The list of unicode values for each keystroke (including surrounding keys)
*/
private final ArrayList<int[]> mCodes;
/**
* The word chosen from the candidate list, until it is committed.
*/
private String mPreferredWord;
private final StringBuilder mTypedWord;
private int mCapsCount;
private boolean mAutoCapitalized;
/**
* Whether the user chose to capitalize the first char of the word.
*/
private boolean mIsFirstCharCapitalized;
public WordComposer() {
mCodes = new ArrayList<int[]>(12);
mTypedWord = new StringBuilder(20);
}
WordComposer(WordComposer copy) {
mCodes = new ArrayList<int[]>(copy.mCodes);
mPreferredWord = copy.mPreferredWord;
mTypedWord = new StringBuilder(copy.mTypedWord);
mCapsCount = copy.mCapsCount;
mAutoCapitalized = copy.mAutoCapitalized;
mIsFirstCharCapitalized = copy.mIsFirstCharCapitalized;
}
/**
* Clear out the keys registered so far.
*/
public void reset() {
mCodes.clear();
mIsFirstCharCapitalized = false;
mPreferredWord = null;
mTypedWord.setLength(0);
mCapsCount = 0;
}
/**
* Number of keystrokes in the composing word.
* @return the number of keystrokes
*/
public int size() {
return mCodes.size();
}
/**
* Returns the codes at a particular position in the word.
* @param index the position in the word
* @return the unicode for the pressed and surrounding keys
*/
public int[] getCodesAt(int index) {
return mCodes.get(index);
}
/**
* Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
* the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
* @param codes the array of unicode values
*/
public void add(int primaryCode, int[] codes) {
mTypedWord.append((char) primaryCode);
correctPrimaryJuxtapos(primaryCode, codes);
mCodes.add(codes);
if (Character.isUpperCase((char) primaryCode)) mCapsCount++;
}
/**
* Swaps the first and second values in the codes array if the primary code is not the first
* value in the array but the second. This happens when the preferred key is not the key that
* the user released the finger on.
* @param primaryCode the preferred character
* @param codes array of codes based on distance from touch point
*/
private void correctPrimaryJuxtapos(int primaryCode, int[] codes) {
if (codes.length < 2) return;
if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) {
codes[1] = codes[0];
codes[0] = primaryCode;
}
}
/**
* Delete the last keystroke as a result of hitting backspace.
*/
public void deleteLast() {
final int codesSize = mCodes.size();
if (codesSize > 0) {
mCodes.remove(codesSize - 1);
final int lastPos = mTypedWord.length() - 1;
char last = mTypedWord.charAt(lastPos);
mTypedWord.deleteCharAt(lastPos);
if (Character.isUpperCase(last)) mCapsCount--;
}
}
/**
* Returns the word as it was typed, without any correction applied.
* @return the word that was typed so far
*/
public CharSequence getTypedWord() {
int wordSize = mCodes.size();
if (wordSize == 0) {
return null;
}
return mTypedWord;
}
public void setFirstCharCapitalized(boolean capitalized) {
mIsFirstCharCapitalized = capitalized;
}
/**
* Whether or not the user typed a capital letter as the first letter in the word
* @return capitalization preference
*/
public boolean isFirstCharCapitalized() {
return mIsFirstCharCapitalized;
}
/**
* Whether or not all of the user typed chars are upper case
* @return true if all user typed chars are upper case, false otherwise
*/
public boolean isAllUpperCase() {
return (mCapsCount > 0) && (mCapsCount == size());
}
/**
* Stores the user's selected word, before it is actually committed to the text field.
* @param preferred
*/
public void setPreferredWord(String preferred) {
mPreferredWord = preferred;
}
/**
* Return the word chosen by the user, or the typed word if no other word was chosen.
* @return the preferred word
*/
public CharSequence getPreferredWord() {
return mPreferredWord != null ? mPreferredWord : getTypedWord();
}
/**
* Returns true if more than one character is upper case, otherwise returns false.
*/
public boolean isMostlyCaps() {
return mCapsCount > 1;
}
/**
* Saves the reason why the word is capitalized - whether it was automatic or
* due to the user hitting shift in the middle of a sentence.
* @param auto whether it was an automatic capitalization due to start of sentence
*/
public void setAutoCapitalized(boolean auto) {
mAutoCapitalized = auto;
}
/**
* Returns whether the word was automatically capitalized.
* @return whether the word was automatically capitalized
*/
public boolean isAutoCapitalized() {
return mAutoCapitalized;
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2010, 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.
*/
-->
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
>
<alpha
android:fromAlpha="0.5"
android:toAlpha="1.0"
android:duration="@integer/config_preview_fadein_anim_time" />
</set>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2010, 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.
*/
-->
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
>
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="@integer/config_preview_fadeout_anim_time" />
</set>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2010, 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.
*/
-->
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
>
<alpha
android:fromAlpha="0.5"
android:toAlpha="1.0"
android:duration="@integer/config_preview_fadein_anim_time" />
</set>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2010, 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.
*/
-->
<set
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator"
>
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="@integer/config_preview_fadeout_anim_time" />
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Some files were not shown because too many files have changed in this diff Show More