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
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| java | ||||
							
								
								
									
										22
									
								
								src/java/KP2ASoftkeyboard_AS/.idea/compiler.xml
									
									
									
										generated
									
									
									
										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> | ||||
							
								
								
									
										3
									
								
								src/java/KP2ASoftkeyboard_AS/.idea/copyright/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| <component name="CopyrightManager"> | ||||
|   <settings default="" /> | ||||
| </component> | ||||
							
								
								
									
										19
									
								
								src/java/KP2ASoftkeyboard_AS/.idea/gradle.xml
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										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
									
									
									
										generated
									
									
									
										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 | 
 Philipp Crocoll
					Philipp Crocoll