convert keyboard project to Android Studio / Gradle project
@ -0,0 +1 @@
|
||||
#Wed Jan 13 21:02:12 CET 2016
|
1
src/java/KP2ASoftkeyboard_AS/.idea/.name
Normal file
@ -0,0 +1 @@
|
||||
java
|
22
src/java/KP2ASoftkeyboard_AS/.idea/compiler.xml
Normal 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>
|
@ -0,0 +1,3 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
19
src/java/KP2ASoftkeyboard_AS/.idea/gradle.xml
Normal 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>
|
38
src/java/KP2ASoftkeyboard_AS/.idea/misc.xml
Normal 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>
|
9
src/java/KP2ASoftkeyboard_AS/.idea/modules.xml
Normal 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>
|
6
src/java/KP2ASoftkeyboard_AS/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="" />
|
||||
</component>
|
||||
</project>
|
1898
src/java/KP2ASoftkeyboard_AS/.idea/workspace.xml
Normal file
19
src/java/KP2ASoftkeyboard_AS/KP2ASoftkeyboard_AS.iml
Normal 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>
|
91
src/java/KP2ASoftkeyboard_AS/app/app.iml
Normal 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>
|
18
src/java/KP2ASoftkeyboard_AS/app/build.gradle
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 = "";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"; }'
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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" : "")));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
After Width: | Height: | Size: 511 B |
After Width: | Height: | Size: 760 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 730 B |
After Width: | Height: | Size: 940 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 332 B |
After Width: | Height: | Size: 498 B |
After Width: | Height: | Size: 811 B |
After Width: | Height: | Size: 715 B |
After Width: | Height: | Size: 1001 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 745 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 833 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 226 B |
After Width: | Height: | Size: 807 B |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 681 B |
After Width: | Height: | Size: 548 B |
After Width: | Height: | Size: 438 B |
After Width: | Height: | Size: 200 B |
After Width: | Height: | Size: 1.0 KiB |