commit 79c310d9b14c65aa6cf2ff87707daeb8ee078340 Author: Yuriy Liskov Date: Thu Feb 12 06:30:30 2026 +0200 Initial commit (recreated history, MIT license) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b1d424 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +/screen.png +notes.txt +/other +/files +/misc +/releases +tmp/ +*_bak* +*_tmp +*.bak* +/.idea +*.apk +*.7z +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild +fabric.properties +*BAK.java diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..359dc59 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020-present Yurii L + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9173e39 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +![Logo of LeanKeyboard](img/leankeykeyboard_logo_small.png "Logo of LeanKeyboard") LeanKeyboard +========= + +[![MPLv2 License](http://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](https://www.mozilla.org/MPL/2.0/) + +__LeanKeyboard: Keyboard for Android-based set-top boxes and TVs:__ + + * Google Play page + * Telegram group + +### Features: + * Designed for TV screens. + * Any remote controller support. + * Supports dozens of languages. + * Doesn't depend on Google Services. + * __No root required!__ + +__Tip: Switch to other language with language button or by long press on the space bar__ + +__Tip: Do long press on the language button to choose between available languages__ + +### Screenshots: + * __[Open screenshots](#screens)__ + +### Install LeanKeyboard: +__Easy installation in less than 10 minutes with only FireTV__ + * Install LeanKeyKeyboard (only FireTV needed) + +__Standard installation via ADB__ + * If you don't know how to sideload/install apps via ADB, read a tutorial (e.g. this one) + * Download latest LeanKeyKeyboard APK and sideload/install with adb: + * *adb install -r LeanKeyboard.apk* + * Enjoy :) + +### Donation: +If you want to support my developments you are welcome to buy me a cup of coffee :) + + + * [**Patreon**](https://www.patreon.com/yuliskov) + * **PayPal**: firsthash at gmail.com + * **BTC**: 1JAT5VVWarVBkpVbNDn8UA8HXNdrukuBSx + * **LTC**: ltc1qgc24eq9jl9cq78qnd5jpqhemkajg9vudwyd8pw + * **ETH**: 0xe455E21a085ae195a097cd4F456051A9916A5064 + * **ETC**: 0x209eCd33Fa61fA92167595eB3Aea92EE1905c815 + * **XMR**: 48QsMjqfkeW54vkgKyRnjodtYxdmLk6HXfTWPSZoaFPEDpoHDwFUciGCe1QC9VAeGrgGw4PKNAksX9RW7myFqYJQDN5cHGT + * **BNB**: bnb1amjr7fauftxxyhe4f95280vklctj243k9u55fq + * **DOGE**: DBnqJwJs2GJBxrCDsi5bXwSmjnz8uGdUpB + * **eUSDT**: 0xe455e21a085ae195a097cd4f456051a9916a5064 + +### Reviews / Articles: + * [__XDA Discussion__](https://forum.xda-developers.com/fire-tv/general/guide-change-screen-keyboard-to-leankey-t3527675) + +### Changelog: + * [Check releases page for changelog ..](https://github.com/yuliskov/LeanKeyboard/releases) + +### Contributors: + * __[aglt](https://github.com/aglt)__ (Icelandic lang) + * __[rabin111](https://github.com/rabin111)__ (Thai lang) + +### Developer: + * __[yuliskov](https://github.com/yuliskov)__ (design & coding) + +### Screens: +![Screenshot of LeanKeyboard](img/leankeykeyboard_screenshot_01.png "Screenshot of LeanKeyboard") +![Screenshot of LeanKeyboard](img/leankeykeyboard_screenshot_02.png "Screenshot of LeanKeyboard") +![Screenshot of LeanKeyboard](img/leankeykeyboard_screenshot_03.png "Screenshot of LeanKeyboard") diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..bde32dc --- /dev/null +++ b/build.gradle @@ -0,0 +1,78 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + // jcenter() + // maven { + // url 'https://maven.google.com/' + // name 'Google' + // } + // google() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.5.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } + // WARNING: don't use 'project.ext' because IDE 'Cannot infer argument type' + // https://stackoverflow.com/questions/20404476/how-to-define-common-android-properties-for-all-modules-using-gradle + // Gradle constants example: https://github.com/google/ExoPlayer + ext { + // Google Play SDK version requirements: + // https://support.google.com/googleplay/android-developer/answer/11926878 + compileSdkVersion = 35 + buildToolsVersion = "35.0.0" + minSdkVersion = 14 + targetSdkVersion = 35 + espressoCoreVersion = 'com.android.support.test.espresso:espresso-core:2.2.2' + junitVersion = 'junit:junit:4.12' + robolectricVersion = 'org.robolectric:robolectric:3.5.1' + crashlyticsVersion = 'com.crashlytics.sdk.android:crashlytics:2.8.0@aar' + // androidx migration: + // https://developer.android.com/jetpack/androidx/migrate + // https://developer.android.com/jetpack/androidx/migrate/artifact-mappings + appCompatXVersion = 'androidx.appcompat:appcompat:1.1.0' + constraintXVersion = 'androidx.constraintlayout:constraintlayout:1.1.3' + supportXVersion = 'androidx.legacy:legacy-support-v4:1.0.0' + leanbackCompatXVersion = 'androidx.leanback:leanback:1.0.0' + designXVersion = 'com.google.android.material:material:1.0.0' + voiceOverlayVersion = 'com.algolia.instantsearch:voice:1.1.0' // https://github.com/algolia/voice-overlay-android + } +} + +allprojects { + repositories { + google() + mavenCentral() + //jcenter() + // com.android.support libs + //maven { url 'https://maven.google.com' } + } + + gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +// Fix 'Namespace not specified' +// https://stackoverflow.com/questions/76300671/android-getting-error-namespace-not-specified +// subprojects { +// afterEvaluate { project -> +// if (project.hasProperty('android')) { +// project.android { +// if (namespace == null) { +// namespace project.group +// } +// } +// } +// } +// } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7f5325b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..93ec5c8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 16 19:59:54 EET 2020 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/img/leankeykeyboard_logo_small.png b/img/leankeykeyboard_logo_small.png new file mode 100644 index 0000000..1c557d5 Binary files /dev/null and b/img/leankeykeyboard_logo_small.png differ diff --git a/img/leankeykeyboard_screenshot_01.png b/img/leankeykeyboard_screenshot_01.png new file mode 100644 index 0000000..b72268f Binary files /dev/null and b/img/leankeykeyboard_screenshot_01.png differ diff --git a/img/leankeykeyboard_screenshot_02.png b/img/leankeykeyboard_screenshot_02.png new file mode 100644 index 0000000..75a9180 Binary files /dev/null and b/img/leankeykeyboard_screenshot_02.png differ diff --git a/img/leankeykeyboard_screenshot_03.png b/img/leankeykeyboard_screenshot_03.png new file mode 100644 index 0000000..e7898e2 Binary files /dev/null and b/img/leankeykeyboard_screenshot_03.png differ diff --git a/img/screen4.png b/img/screen4.png new file mode 100644 index 0000000..839968b Binary files /dev/null and b/img/screen4.png differ diff --git a/img/screen5.png b/img/screen5.png new file mode 100644 index 0000000..cef39ca Binary files /dev/null and b/img/screen5.png differ diff --git a/leankeykeyboard/.gitignore b/leankeykeyboard/.gitignore new file mode 100644 index 0000000..4abf444 --- /dev/null +++ b/leankeykeyboard/.gitignore @@ -0,0 +1,3 @@ +/build +/playstore +/origin diff --git a/leankeykeyboard/build.gradle b/leankeykeyboard/build.gradle new file mode 100644 index 0000000..a9e5e13 --- /dev/null +++ b/leankeykeyboard/build.gradle @@ -0,0 +1,86 @@ +apply plugin: 'com.android.application' + +android { + // Latest gradle fix: https://stackoverflow.com/questions/76300671/android-getting-error-namespace-not-specified + namespace 'com.liskovsoft.leankeykeyboard' + + // Latest gradle fix: https://stackoverflow.com/questions/22604627/gradle-buildconfigfield-buildconfig-cannot-resolve-symbol + buildFeatures { + buildConfig = true + } + + // FIX: Default interface methods are only supported starting with Android N (--min-api 24) + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + compileSdkVersion project.properties.compileSdkVersion + buildToolsVersion project.properties.buildToolsVersion + + defaultConfig { + applicationId "org.liskovsoft.leankeykeyboard.pro" + minSdkVersion project.properties.minSdkVersion + targetSdkVersion project.properties.targetSdkVersion + versionCode 205 + versionName "6.1.35" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + // https://medium.com/@angelhiadefiesta/how-to-obfuscate-in-android-with-proguard-acab47701577 + minifyEnabled true // enable obfuscation + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + // naming example: SmartYouTubeTV_Xwalk_v6.8.12_r.apk + // https://stackoverflow.com/questions/18332474/how-to-set-versionname-in-apk-filename-using-gradle + applicationVariants.all { variant -> + variant.outputs.each { output -> + def project = "LeanKeyboard" + // Latest gradle fix: https://stackoverflow.com/questions/62075122/no-such-property-variantconfiguration-for-class + def buildType = variant.buildType.name.take(1) + def version = variant.versionName + def flavor = variant.productFlavors[-1].name + + def newApkName = sprintf("%s_v%s_%s_%s.apk", [project, version, flavor, buildType]) + + output.outputFileName = new File(newApkName) + } + } + + lintOptions { + abortOnError true + disable 'MissingTranslation' + disable 'NewApi' + } + + // gradle 4.6 migration: disable dimensions mechanism + // more: https://proandroiddev.com/advanced-android-flavors-part-4-a-new-version-fc2ad80c01bb + flavorDimensions "default" + + productFlavors { + playstore { + applicationId "org.liskovsoft.androidtv.rukeyboard" + } + origin { + applicationId "com.liskovsoft.leankeyboard" + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + androidTestImplementation(project.properties.espressoCoreVersion, { + exclude group: 'com.android.support', module: 'support-annotations' + }) + testImplementation project.properties.junitVersion + implementation project.properties.appCompatXVersion + implementation project.properties.leanbackCompatXVersion + implementation project.properties.constraintXVersion + implementation project.properties.designXVersion + implementation project.properties.voiceOverlayVersion +} diff --git a/leankeykeyboard/proguard-rules.pro b/leankeykeyboard/proguard-rules.pro new file mode 100644 index 0000000..57bc3d6 --- /dev/null +++ b/leankeykeyboard/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\portable\dev\android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/leankeykeyboard/src/androidTest/java/com/liskovsoft/leankeykeyboard/ExampleInstrumentedTest.java b/leankeykeyboard/src/androidTest/java/com/liskovsoft/leankeykeyboard/ExampleInstrumentedTest.java new file mode 100644 index 0000000..4a3fad0 --- /dev/null +++ b/leankeykeyboard/src/androidTest/java/com/liskovsoft/leankeykeyboard/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.liskovsoft.leankeykeyboard; + +import android.content.Context; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.liskovsoft.leankeykeyboard", appContext.getPackageName()); + } +} diff --git a/leankeykeyboard/src/main/AndroidManifest.xml b/leankeykeyboard/src/main/AndroidManifest.xml new file mode 100644 index 0000000..653ab3a --- /dev/null +++ b/leankeykeyboard/src/main/AndroidManifest.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/PermissionsActivity.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/PermissionsActivity.java new file mode 100644 index 0000000..a8275e5 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/PermissionsActivity.java @@ -0,0 +1,43 @@ +package com.liskovsoft.leankeyboard.activity; + +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import com.liskovsoft.leankeyboard.helpers.PermissionHelpers; +import com.liskovsoft.leankeyboard.receiver.RestartServiceReceiver; + +public class PermissionsActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + checkPermissions(); + } + + @Override + protected void onStop() { + super.onStop(); + + // restart kbd service + Intent intent = new Intent(this, RestartServiceReceiver.class); + sendBroadcast(intent); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + checkPermissions(); + } + + private void checkPermissions() { + if (!PermissionHelpers.hasMicPermissions(this)) { + PermissionHelpers.verifyMicPermissions(this); + } else if (!PermissionHelpers.hasStoragePermissions(this)) { + PermissionHelpers.verifyStoragePermissions(this); + } else { + finish(); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbLayoutActivity.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbLayoutActivity.java new file mode 100644 index 0000000..9def7fb --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbLayoutActivity.java @@ -0,0 +1,26 @@ +package com.liskovsoft.leankeyboard.activity.settings; + +import android.content.Intent; +import android.os.Bundle; +import androidx.fragment.app.FragmentActivity; +import androidx.leanback.app.GuidedStepSupportFragment; +import com.liskovsoft.leankeyboard.fragments.settings.KbLayoutFragment; +import com.liskovsoft.leankeyboard.receiver.RestartServiceReceiver; + +public class KbLayoutActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + GuidedStepSupportFragment.addAsRoot(this, new KbLayoutFragment(), android.R.id.content); + } + + @Override + protected void onStop() { + super.onStop(); + + // restart kbd service + Intent intent = new Intent(this, RestartServiceReceiver.class); + sendBroadcast(intent); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbSettingsActivity.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbSettingsActivity.java new file mode 100644 index 0000000..c6cc187 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbSettingsActivity.java @@ -0,0 +1,26 @@ +package com.liskovsoft.leankeyboard.activity.settings; + +import android.content.Intent; +import android.os.Bundle; +import androidx.fragment.app.FragmentActivity; +import androidx.leanback.app.GuidedStepSupportFragment; +import com.liskovsoft.leankeyboard.fragments.settings.KbSettingsFragment; +import com.liskovsoft.leankeyboard.receiver.RestartServiceReceiver; + +public class KbSettingsActivity extends FragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + GuidedStepSupportFragment.addAsRoot(this, new KbSettingsFragment(), android.R.id.content); + } + + @Override + protected void onStop() { + super.onStop(); + + // restart kbd service + Intent intent = new Intent(this, RestartServiceReceiver.class); + sendBroadcast(intent); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbSettingsActivity2.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbSettingsActivity2.java new file mode 100644 index 0000000..afbfa2b --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/activity/settings/KbSettingsActivity2.java @@ -0,0 +1,4 @@ +package com.liskovsoft.leankeyboard.activity.settings; + +public class KbSettingsActivity2 extends KbSettingsActivity { +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardBuilder.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardBuilder.java new file mode 100644 index 0000000..85e2bc3 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardBuilder.java @@ -0,0 +1,10 @@ +package com.liskovsoft.leankeyboard.addons.keyboards; + +import android.inputmethodservice.Keyboard; +import androidx.annotation.Nullable; + +public interface KeyboardBuilder { + Keyboard createAbcKeyboard(); + Keyboard createSymKeyboard(); + Keyboard createNumKeyboard(); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardFactory.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardFactory.java new file mode 100644 index 0000000..e237be1 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardFactory.java @@ -0,0 +1,10 @@ +package com.liskovsoft.leankeyboard.addons.keyboards; + +import android.content.Context; + +import java.util.List; + +public interface KeyboardFactory { + List getAllAvailableKeyboards(Context context); + boolean needUpdate(); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardInfo.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardInfo.java new file mode 100644 index 0000000..800b50a --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardInfo.java @@ -0,0 +1,12 @@ +package com.liskovsoft.leankeyboard.addons.keyboards; + +public interface KeyboardInfo { + String getLangCode(); + void setLangCode(String langCode); + String getLangName(); + void setLangName(String langName); + boolean isEnabled(); + void setEnabled(boolean enabled); + boolean isAzerty(); + void setIsAzerty(boolean enabled); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardManager.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardManager.java new file mode 100644 index 0000000..c0947e9 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardManager.java @@ -0,0 +1,103 @@ +package com.liskovsoft.leankeyboard.addons.keyboards; + +import android.content.Context; +import android.inputmethodservice.Keyboard; +import com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards.ResKeyboardFactory; + +import java.util.ArrayList; +import java.util.List; + +public class KeyboardManager { + private final Context mContext; + private final KeyboardStateManager mStateManager; + private List mKeyboardBuilders; + private List mAllKeyboards; + private final KeyboardFactory mKeyboardFactory; + private int mKeyboardIndex = 0; + + public static class KeyboardData { + public Keyboard abcKeyboard; + public Keyboard symKeyboard; + public Keyboard numKeyboard; + } + + public KeyboardManager(Context ctx) { + mContext = ctx; + mStateManager = new KeyboardStateManager(mContext, this); + mKeyboardFactory = new ResKeyboardFactory(mContext); + mStateManager.restore(); + } + + public void load() { + mKeyboardBuilders = mKeyboardFactory.getAllAvailableKeyboards(mContext); + mAllKeyboards = buildAllKeyboards(); + } + + private List buildAllKeyboards() { + List keyboards = new ArrayList<>(); + if (!mKeyboardBuilders.isEmpty()) { + for (KeyboardBuilder builder : mKeyboardBuilders) { + KeyboardData data = new KeyboardData(); + data.abcKeyboard = builder.createAbcKeyboard(); + data.symKeyboard = builder.createSymKeyboard(); + data.numKeyboard = builder.createNumKeyboard(); + + keyboards.add(data); + } + } + return keyboards; + } + + /** + * Performs callback to event handlers + */ + private void onNextKeyboard() { + mStateManager.onNextKeyboard(); + } + + /** + * Get next keyboard from internal source (looped) + */ + public KeyboardData next() { + if (mKeyboardFactory.needUpdate() || mAllKeyboards == null) { + load(); + } + + ++mKeyboardIndex; + + mKeyboardIndex = mKeyboardIndex < mAllKeyboards.size() ? mKeyboardIndex : 0; + + KeyboardData kbd = mAllKeyboards.get(mKeyboardIndex); + + if (kbd == null) { + throw new IllegalStateException(String.format("Keyboard %s not initialized", mKeyboardIndex)); + } + + onNextKeyboard(); + + return kbd; + } + + public int getIndex() { + return mKeyboardIndex; + } + + public void setIndex(int idx) { + mKeyboardIndex = idx; + } + + /** + * Get current keyboard + */ + public KeyboardData get() { + if (mAllKeyboards == null) { + load(); + } + + if (mAllKeyboards.size() <= mKeyboardIndex) { + mKeyboardIndex = 0; + } + + return mAllKeyboards.get(mKeyboardIndex); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardStateManager.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardStateManager.java new file mode 100644 index 0000000..a6fb483 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/KeyboardStateManager.java @@ -0,0 +1,25 @@ +package com.liskovsoft.leankeyboard.addons.keyboards; + +import android.content.Context; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; + +public class KeyboardStateManager { + private final Context mContext; + private final KeyboardManager mManager; + private final LeanKeyPreferences mPrefs; + + public KeyboardStateManager(Context context, KeyboardManager manager) { + mContext = context; + mManager = manager; + mPrefs = LeanKeyPreferences.instance(mContext); + } + + public void restore() { + int idx = mPrefs.getKeyboardIndex(); + mManager.setIndex(idx); + } + + public void onNextKeyboard() { + mPrefs.setKeyboardIndex(mManager.getIndex()); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOn.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOn.java new file mode 100644 index 0000000..968dccc --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOn.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface AddOn { + interface AddOnResourceMapping { + /*@AttrRes + int getLocalAttrIdFromRemote(@AttrRes int remoteAttributeResourceId);*/ + + int[] getRemoteStyleableArrayFromLocal(int[] localStyleableArray); + } + + int INVALID_RES_ID = 0; + + String getId(); + + String getName(); + + String getDescription(); + + String getPackageName(); + + @Nullable + Context getPackageContext(); + + int getSortIndex(); + + @NonNull + AddOnResourceMapping getResourceMapping(); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOnImpl.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOnImpl.java new file mode 100644 index 0000000..334b0ae --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOnImpl.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.SparseArrayCompat; +import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log.Logger; + +import java.lang.ref.WeakReference; +import java.util.Arrays; + +public abstract class AddOnImpl implements AddOn { + + private static final String TAG = "ASK_AddOnImpl"; + private final String mId; + private final String mName; + private final String mDescription; + private final String mPackageName; + private final Context mAskAppContext; + private WeakReference mPackageContext; + private final int mSortIndex; + private final AddOnResourceMappingImpl mAddOnResourceMapping; + + protected AddOnImpl(Context askContext, Context packageContext, String id, int nameResId, + String description, int sortIndex) { + mId = id; + mAskAppContext = askContext; + mName = packageContext.getString(nameResId); + mDescription = description; + mPackageName = packageContext.getPackageName(); + mPackageContext = new WeakReference<>(packageContext); + mSortIndex = sortIndex; + mAddOnResourceMapping = new AddOnResourceMappingImpl(this); + } + + public final String getId() { + return mId; + } + + public final String getDescription() { + return mDescription; + } + + public String getPackageName() { + return mPackageName; + } + + @Nullable + public final Context getPackageContext() { + Context c = mPackageContext.get(); + if (c == null) { + try { + c = mAskAppContext.createPackageContext(mPackageName, Context.CONTEXT_IGNORE_SECURITY); + mPackageContext = new WeakReference<>(c); + } catch (NameNotFoundException e) { + Logger.w(TAG, "Failed to find package %s!", mPackageName); + Logger.w(TAG, "Failed to find package! ", e); + } + } + return c; + } + + public final int getSortIndex() { + return mSortIndex; + } + + public String getName() { + return mName; + } + + @Override + public int hashCode() { + return getId().hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof AddOn && + ((AddOn) o).getId().equals(getId()); + } + + @NonNull + @Override + public AddOnResourceMapping getResourceMapping() { + return mAddOnResourceMapping; + } + + private static class AddOnResourceMappingImpl implements AddOnResourceMapping { + private final WeakReference mAddOnWeakReference; + private final SparseIntArray mAttributesMapping = new SparseIntArray(); + private final SparseArrayCompat mStyleableArrayMapping = new SparseArrayCompat<>(); + + private AddOnResourceMappingImpl(@NonNull AddOnImpl addOn) { + mAddOnWeakReference = new WeakReference<>(addOn); + } + + @Override + public int[] getRemoteStyleableArrayFromLocal(int[] localStyleableArray) { + int localStyleableId = Arrays.hashCode(localStyleableArray); + int indexOfRemoteArray = mStyleableArrayMapping.indexOfKey(localStyleableId); + if (indexOfRemoteArray >= 0) return mStyleableArrayMapping.valueAt(indexOfRemoteArray); + AddOnImpl addOn = mAddOnWeakReference.get(); + if (addOn == null) return new int[0]; + Context askContext = addOn.mAskAppContext; + Context remoteContext = addOn.getPackageContext(); + if (remoteContext == null) return new int[0]; + int[] remoteAttrIds = Support.createBackwardCompatibleStyleable(localStyleableArray, askContext, remoteContext, mAttributesMapping); + mStyleableArrayMapping.put(localStyleableId, remoteAttrIds); + return remoteAttrIds; + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOnsFactory.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOnsFactory.java new file mode 100644 index 0000000..90fffd8 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/AddOnsFactory.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.util.AttributeSet; +import android.util.Xml; + +//import com.liskovsoft.keyboardaddons.apklangfactory.AnySoftKeyboard; +import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log.Logger; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +public abstract class AddOnsFactory { + + private static final class AddOnsComparator implements Comparator { + private final String mAskPackageName; + + private AddOnsComparator(Context askContext) { + mAskPackageName = askContext.getPackageName(); + } + + public int compare(AddOn k1, AddOn k2) { + String c1 = k1.getPackageName(); + String c2 = k2.getPackageName(); + + if (c1.equals(c2)) + return k1.getSortIndex() - k2.getSortIndex(); + else if (c1.equals(mAskPackageName))//I want to make sure ASK packages are first + return -1; + else if (c2.equals(mAskPackageName)) + return 1; + else + return c1.compareToIgnoreCase(c2); + } + } + + private final static ArrayList> mActiveInstances = new ArrayList<>(); + + private static final String sTAG = "AddOnsFactory"; + + public static AddOn locateAddOn(String id, Context askContext) { + for (AddOnsFactory factory : mActiveInstances) { + AddOn addOn = factory.getAddOnById(id, askContext); + if (addOn != null) { + Logger.d(sTAG, "Located addon with id " + addOn.getId() + " of type " + addOn.getClass().getName()); + return addOn; + } + } + + return null; + } + + protected final String TAG; + + /** + * This is the interface name that a broadcast receiver implementing an + * external addon should say that it supports -- that is, this is the + * action it uses for its intent filter. + */ + private final String RECEIVER_INTERFACE; + + /** + * Name under which an external addon broadcast receiver component + * publishes information about itself. + */ + private final String RECEIVER_META_DATA; + + private final ArrayList mAddOns = new ArrayList<>(); + private final HashMap mAddOnsById = new HashMap<>(); + + private final boolean mReadExternalPacksToo; + private final String ROOT_NODE_TAG; + private final String ADDON_NODE_TAG; + //private final int mBuildInAddOnsResId; + + private static final String XML_PREF_ID_ATTRIBUTE = "id"; + private static final String XML_NAME_RES_ID_ATTRIBUTE = "nameResId"; + private static final String XML_DESCRIPTION_ATTRIBUTE = "description"; + private static final String XML_SORT_INDEX_ATTRIBUTE = "index"; + + protected AddOnsFactory(String tag, String receiverInterface, String receiverMetaData, String rootNodeTag, String addonNodeTag, int buildInAddonResId, boolean readExternalPacksToo) { + TAG = tag; + RECEIVER_INTERFACE = receiverInterface; + RECEIVER_META_DATA = receiverMetaData; + ROOT_NODE_TAG = rootNodeTag; + ADDON_NODE_TAG = addonNodeTag; + //mBuildInAddOnsResId = buildInAddonResId; + mReadExternalPacksToo = readExternalPacksToo; + + mActiveInstances.add(this); + } + + protected boolean isEventRequiresCacheRefresh(Intent eventIntent, Context context) { + String action = eventIntent.getAction(); + String packageNameSchemePart = eventIntent.getData().getSchemeSpecificPart(); + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + //will reset only if the new package has my addons + boolean hasAddon = isPackageContainAnAddon(context, packageNameSchemePart); + if (hasAddon) { + Logger.d(TAG, "It seems that an addon exists in a newly installed package " + packageNameSchemePart + ". I need to reload stuff."); + return true; + } + } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action) || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { + //If I'm managing OR it contains an addon (could be new feature in the package), I want to reset. + boolean isPackagedManaged = isPackageManaged(packageNameSchemePart); + if (isPackagedManaged) { + Logger.d(TAG, "It seems that an addon I use (in package " + packageNameSchemePart + ") has been changed. I need to reload stuff."); + return true; + } else { + boolean hasAddon = isPackageContainAnAddon(context, packageNameSchemePart); + if (hasAddon) { + Logger.d(TAG, "It seems that an addon exists in an updated package " + packageNameSchemePart + ". I need to reload stuff."); + return true; + } + } + } else //removed + { + //so only if I manage this package, I want to reset + boolean isPackagedManaged = isPackageManaged(packageNameSchemePart); + if (isPackagedManaged) { + Logger.d(TAG, "It seems that an addon I use (in package " + packageNameSchemePart + ") has been removed. I need to reload stuff."); + return true; + } + } + return false; + } + + protected boolean isPackageManaged(String packageNameSchemePart) { + for (AddOn addOn : mAddOns) { + if (addOn.getPackageName().equals(packageNameSchemePart)) { + return true; + } + } + + return false; + } + + protected boolean isPackageContainAnAddon(Context context, String packageNameSchemePart) { + PackageInfo newPackage; + try { + newPackage = context.getPackageManager().getPackageInfo(packageNameSchemePart, PackageManager.GET_RECEIVERS + PackageManager + .GET_META_DATA); + } catch (NameNotFoundException e) { + e.printStackTrace(); + return false; + } + if (newPackage.receivers != null) { + ActivityInfo[] receivers = newPackage.receivers; + for (ActivityInfo aReceiver : receivers) { + //issue 904 + if (aReceiver == null || aReceiver.applicationInfo == null || !aReceiver.enabled || !aReceiver.applicationInfo.enabled) + continue; + final XmlPullParser xml = aReceiver.loadXmlMetaData(context.getPackageManager(), RECEIVER_META_DATA); + if (xml != null) { + return true; + } + } + } + + return false; + } + + protected boolean isEventRequiresViewReset(Intent eventIntent, Context context) { + return false; + } + + protected synchronized void clearAddOnList() { + mAddOns.clear(); + mAddOnsById.clear(); + } + + public synchronized E getAddOnById(String id, Context askContext) { + if (mAddOnsById.size() == 0) { + loadAddOns(askContext); + } + return mAddOnsById.get(id); + } + + public synchronized final List getAllAddOns(Context askContext) { + Logger.d(TAG, "getAllAddOns has %d add on for %s", mAddOns.size(), getClass().getName()); + if (mAddOns.size() == 0) { + loadAddOns(askContext); + } + Logger.d(TAG, "getAllAddOns will return %d add on for %s", mAddOns.size(), getClass().getName()); + return Collections.unmodifiableList(mAddOns); + } + + protected void loadAddOns(final Context askContext) { + clearAddOnList(); + + //ArrayList local = getAddOnsFromResId(askContext, askContext, mBuildInAddOnsResId); + //for (E addon : local) { + // Logger.d(TAG, "Local add-on %s loaded", addon.getId()); + //} + //mAddOns.addAll(local); + ArrayList external = getExternalAddOns(askContext); + for (E addon : external) { + Logger.d(TAG, "External add-on %s loaded", addon.getId()); + } + mAddOns.addAll(external); + Logger.d(TAG, "Have %d add on for %s", mAddOns.size(), getClass().getName()); + + buildOtherDataBasedOnNewAddOns(mAddOns); + + //sorting the keyboards according to the requested + //sort order (from minimum to maximum) + Collections.sort(mAddOns, new AddOnsComparator(askContext)); + Logger.d(TAG, "Have %d add on for %s (after sort)", mAddOns.size(), getClass().getName()); + } + + protected void buildOtherDataBasedOnNewAddOns(ArrayList newAddOns) { + for (E addOn : newAddOns) + mAddOnsById.put(addOn.getId(), addOn); + } + + private ArrayList getExternalAddOns(Context askContext) { + final ArrayList externalAddOns = new ArrayList<>(); + + if (!mReadExternalPacksToo)//this will disable external packs (API careful stage) + return externalAddOns; + + final List broadcastReceivers = + askContext.getPackageManager().queryBroadcastReceivers(new Intent(RECEIVER_INTERFACE), PackageManager.GET_META_DATA); + + + for (final ResolveInfo receiver : broadcastReceivers) { + if (receiver.activityInfo == null) { + Logger.e(TAG, "BroadcastReceiver has null ActivityInfo. Receiver's label is " + + receiver.loadLabel(askContext.getPackageManager())); + Logger.e(TAG, "Is the external keyboard a service instead of BroadcastReceiver?"); + // Skip to next receiver + continue; + } + + if (!receiver.activityInfo.enabled || !receiver.activityInfo.applicationInfo.enabled) continue; + + try { + final Context externalPackageContext = askContext.createPackageContext(receiver.activityInfo.packageName, Context.CONTEXT_IGNORE_SECURITY); + final ArrayList packageAddOns = getAddOnsFromActivityInfo(askContext, externalPackageContext, receiver.activityInfo); + + externalAddOns.addAll(packageAddOns); + } catch (final NameNotFoundException e) { + Logger.e(TAG, "Did not find package: " + receiver.activityInfo.packageName); + } + + } + + return externalAddOns; + } + + private ArrayList getAddOnsFromResId(Context askContext, Context context, int addOnsResId) { + final XmlPullParser xml = context.getResources().getXml(addOnsResId); + if (xml == null) + return new ArrayList<>(); + return parseAddOnsFromXml(askContext, context, xml); + } + + private ArrayList getAddOnsFromActivityInfo(Context askContext, Context context, ActivityInfo ai) { + final XmlPullParser xml = ai.loadXmlMetaData(context.getPackageManager(), RECEIVER_META_DATA); + if (xml == null)//issue 718: maybe a bad package? + return new ArrayList<>(); + return parseAddOnsFromXml(askContext, context, xml); + } + + private ArrayList parseAddOnsFromXml(Context askContext, Context context, XmlPullParser xml) { + final ArrayList addOns = new ArrayList<>(); + try { + int event; + boolean inRoot = false; + while ((event = xml.next()) != XmlPullParser.END_DOCUMENT) { + final String tag = xml.getName(); + if (event == XmlPullParser.START_TAG) { + if (ROOT_NODE_TAG.equals(tag)) { + inRoot = true; + } else if (inRoot && ADDON_NODE_TAG.equals(tag)) { + final AttributeSet attrs = Xml.asAttributeSet(xml); + E addOn = createAddOnFromXmlAttributes(askContext, attrs, context); + if (addOn != null) { + addOns.add(addOn); + } + } + } else if (event == XmlPullParser.END_TAG) { + if (ROOT_NODE_TAG.equals(tag)) { + inRoot = false; + break; + } + } + } + } catch (final IOException e) { + Logger.e(TAG, "IO error:" + e); + e.printStackTrace(); + } catch (final XmlPullParserException e) { + Logger.e(TAG, "Parse error:" + e); + e.printStackTrace(); + } + + return addOns; + } + + private E createAddOnFromXmlAttributes(Context askContext, AttributeSet attrs, Context context) { + final String prefId = attrs.getAttributeValue(null, XML_PREF_ID_ATTRIBUTE); + final int nameId = attrs.getAttributeResourceValue(null, XML_NAME_RES_ID_ATTRIBUTE, AddOn.INVALID_RES_ID); + final int descriptionInt = attrs.getAttributeResourceValue(null, XML_DESCRIPTION_ATTRIBUTE, AddOn.INVALID_RES_ID); + //NOTE, to be compatible we need this. because the most of descriptions are + //without @string/adb + String description; + if (descriptionInt != AddOn.INVALID_RES_ID) { + description = context.getResources().getString(descriptionInt); + } else { + description = attrs.getAttributeValue(null, XML_DESCRIPTION_ATTRIBUTE); + } + + final int sortIndex = attrs.getAttributeUnsignedIntValue(null, XML_SORT_INDEX_ATTRIBUTE, 1); + + // asserting + if ((prefId == null) || (nameId == AddOn.INVALID_RES_ID)) { + Logger.e(TAG, "External add-on does not include all mandatory details! Will not create add-on."); + return null; + } else { + Logger.d(TAG, "External addon details: prefId:" + prefId + " nameId:" + nameId); + return createConcreteAddOn(askContext, context, prefId, nameId, description, sortIndex, attrs); + } + } + + protected abstract E createConcreteAddOn(Context askContext, Context context, String prefId, int nameId, String description, int sortIndex, AttributeSet attrs); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/Support.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/Support.java new file mode 100644 index 0000000..1484c60 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/addons/Support.java @@ -0,0 +1,64 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons; + +import android.content.Context; +import android.content.res.Resources; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; +import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log.Logger; + +import java.util.ArrayList; +import java.util.List; + +class Support { + private static final String TAG = Support.class.getName(); + + /** + * Creates a mapping between the local styleable and the remote. + * @param localStyleableArray the local styleable to map against + * @param localContext local APK's Context + * @param remoteContext remote package's Context + * @param attributeIdMap a mapping between the remote-id -> local-id + * @return Always returns the remote version of localStyleableArray + */ + public static int[] createBackwardCompatibleStyleable(@NonNull int[] localStyleableArray, @NonNull Context localContext, @NonNull Context remoteContext, @NonNull SparseIntArray attributeIdMap) { + if (localContext == null) throw new NullPointerException("askContext can not be null"); + if (remoteContext == null) throw new NullPointerException("context can not be null"); + + final String remotePackageName = remoteContext.getPackageName(); + if (localContext.getPackageName().equals(remotePackageName)) { + Logger.d(TAG, "This is a local context ("+remotePackageName+"), optimization will be done."); + //optimization + for(int attrId : localStyleableArray) { + attributeIdMap.put(attrId, attrId); + } + return localStyleableArray; + } + final Resources localRes = localContext.getResources(); + final Resources remoteRes = remoteContext.getResources(); + List styleableIdList = new ArrayList<>(localStyleableArray.length); + for(int attrId : localStyleableArray) { + final boolean isAndroidAttribute = localRes.getResourcePackageName(attrId).equals("android"); + final int remoteAttrId; + + if (isAndroidAttribute) { + //android attribute IDs are the same always. So, I can optimize. + remoteAttrId = attrId; + } else { + final String attributeName = localRes.getResourceEntryName(attrId); + remoteAttrId = remoteRes.getIdentifier(attributeName, "attr", remotePackageName); + Logger.d(TAG, "attr "+attributeName+", local id "+attrId+", remote id "+remoteAttrId); + } + if (remoteAttrId != 0) { + attributeIdMap.put(remoteAttrId, attrId); + styleableIdList.add(remoteAttrId); + } + } + final int[] remoteMappedStyleable = new int[styleableIdList.size()]; + for(int i=0; i implements KeyboardFactory { + private static final String TAG = "ASK_KF"; + + private static final String XML_LAYOUT_RES_ID_ATTRIBUTE = "layoutResId"; + private static final String XML_LANDSCAPE_LAYOUT_RES_ID_ATTRIBUTE = "landscapeResId"; + private static final String XML_ICON_RES_ID_ATTRIBUTE = "iconResId"; + private static final String XML_DICTIONARY_NAME_ATTRIBUTE = "defaultDictionaryLocale"; + private static final String XML_ADDITIONAL_IS_LETTER_EXCEPTIONS_ATTRIBUTE = "additionalIsLetterExceptions"; + private static final String XML_SENTENCE_SEPARATOR_CHARACTERS_ATTRIBUTE = "sentenceSeparators"; + private static final String DEFAULT_SENTENCE_SEPARATORS = ".,!?)]:;"; + private static final String XML_PHYSICAL_TRANSLATION_RES_ID_ATTRIBUTE = "physicalKeyboardMappingResId"; + private static final String XML_DEFAULT_ATTRIBUTE = "defaultEnabled"; + + public ApkLangKeyboardFactory() { + super(TAG, "com.liskovsoft.leankey.langpack.KEYBOARD", "com.liskovsoft.leankey.langpack.keyboards", + "Keyboards", "Keyboard", + 0, true); + } + + @Override + public List getAllAvailableKeyboards(Context context) { + return getAllAddOns(context); + } + + @Override + public boolean needUpdate() { + // TODO: implement need update + return false; + } + + public List getEnabledKeyboards(Context askContext) { + final List allAddOns = getAllAddOns(askContext); + Logger.i(TAG, "Creating enabled addons list. I have a total of " + allAddOns.size() + " addons"); + + //getting shared prefs to determine which to create. + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(askContext); + + final ArrayList enabledAddOns = new ArrayList<>(); + for (int addOnIndex = 0; addOnIndex < allAddOns.size(); addOnIndex++) { + final ApkKeyboardAddOnAndBuilder addOn = allAddOns.get(addOnIndex); + + final boolean addOnEnabled = sharedPreferences.getBoolean(addOn.getId(), addOn.getKeyboardDefaultEnabled()); + + if (addOnEnabled) { + enabledAddOns.add(addOn); + } + } + + // Fix: issue 219 + // Check if there is any keyboards created if not, lets create a default english keyboard + if (enabledAddOns.size() == 0) { + final SharedPreferences.Editor editor = sharedPreferences.edit(); + final ApkKeyboardAddOnAndBuilder addOn = allAddOns.get(0); + editor.putBoolean(addOn.getId(), true); + editor.commit(); + enabledAddOns.add(addOn); + } + + for (final ApkKeyboardAddOnAndBuilder addOn : enabledAddOns) { + Logger.d(TAG, "Factory provided addon: %s", addOn.getId()); + } + + return enabledAddOns; + } + + @Override + protected ApkKeyboardAddOnAndBuilder createConcreteAddOn(Context askContext, Context context, String prefId, int nameId, String description, int sortIndex, AttributeSet attrs) { + final int layoutResId = attrs.getAttributeResourceValue(null, XML_LAYOUT_RES_ID_ATTRIBUTE, AddOn.INVALID_RES_ID); + final int landscapeLayoutResId = attrs.getAttributeResourceValue(null, XML_LANDSCAPE_LAYOUT_RES_ID_ATTRIBUTE, AddOn.INVALID_RES_ID); + //final int iconResId = attrs.getAttributeResourceValue(null, XML_ICON_RES_ID_ATTRIBUTE, R.drawable.sym_keyboard_notification_icon); + final int iconResId = 0; + final String defaultDictionary = attrs.getAttributeValue(null, XML_DICTIONARY_NAME_ATTRIBUTE); + final String additionalIsLetterExceptions = attrs.getAttributeValue(null, XML_ADDITIONAL_IS_LETTER_EXCEPTIONS_ATTRIBUTE); + String sentenceSeparators = attrs.getAttributeValue(null, XML_SENTENCE_SEPARATOR_CHARACTERS_ATTRIBUTE); + if (TextUtils.isEmpty(sentenceSeparators)) sentenceSeparators = DEFAULT_SENTENCE_SEPARATORS; + final int physicalTranslationResId = attrs.getAttributeResourceValue(null, XML_PHYSICAL_TRANSLATION_RES_ID_ATTRIBUTE, AddOn.INVALID_RES_ID); + // A keyboard is enabled by default if it is the first one (index==1) + final boolean keyboardDefault = attrs.getAttributeBooleanValue(null, XML_DEFAULT_ATTRIBUTE, sortIndex == 1); + + // asserting + if ((prefId == null) || (nameId == AddOn.INVALID_RES_ID) || (layoutResId == AddOn.INVALID_RES_ID)) { + Logger.e(TAG, "External Keyboard does not include all mandatory details! Will not create keyboard."); + return null; + } else { + Logger.d(TAG, + "External keyboard details: prefId:" + prefId + " nameId:" + + nameId + " resId:" + layoutResId + + " landscapeResId:" + landscapeLayoutResId + + " iconResId:" + iconResId + " defaultDictionary:" + + defaultDictionary); + return new ApkKeyboardAddOnAndBuilder(askContext, context, + prefId, nameId, layoutResId, landscapeLayoutResId, + defaultDictionary, iconResId, physicalTranslationResId, + additionalIsLetterExceptions, sentenceSeparators, + description, sortIndex, keyboardDefault); + } + } + + public boolean hasMultipleAlphabets(Context askContext) { + return getEnabledKeyboards(askContext).size() > 1; + } + + public Keyboard createKeyboard(Context context) { + List keyboardBuilders = getAllAvailableKeyboards(context); + if (keyboardBuilders.size() == 0) + return new Keyboard(context, 0x7f04000c); // ru keyboard resource id + // remember, only one external keyboard supported + return keyboardBuilders.get(0).createAbcKeyboard(); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/BuildConfig.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/BuildConfig.java new file mode 100644 index 0000000..a52b1e9 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/BuildConfig.java @@ -0,0 +1,6 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log; + +public class BuildConfig { + public final static boolean TESTING_BUILD = true; + public final static boolean DEBUG = true; +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/LogCatLogProvider.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/LogCatLogProvider.java new file mode 100644 index 0000000..34d9eec --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/LogCatLogProvider.java @@ -0,0 +1,48 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log; + +import android.os.Build; +import android.util.Log; + +/** + * Logger messages to Android's LogCat. Should be used only in DEBUG builds. + */ +public class LogCatLogProvider implements LogProvider { + @Override + public void v(String TAG, String text) { + Log.v(TAG, text); + } + + @Override + public void d(String TAG, String text) { + Log.d(TAG, text); + } + + @Override + public void yell(String TAG, String text) { + Log.w(TAG+"-YELL", text); + } + + @Override + public void i(String TAG, String text) { + Log.i(TAG, text); + } + + @Override + public void w(String TAG, String text) { + Log.w(TAG, text); + } + + @Override + public void e(String TAG, String text) { + Log.e(TAG, text); + } + + @Override + public void wtf(String TAG, String text) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { + Log.wtf(TAG, text); + } else { + Log.e(TAG+" WTF", text); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/LogProvider.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/LogProvider.java new file mode 100644 index 0000000..b0c66a6 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/LogProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log; + +public interface LogProvider { + + void v(String TAG, String text); + + void d(String TAG, String text); + + void yell(String TAG, String text); + + void i(String TAG, String text); + + void w(String TAG, String text); + + void e(String TAG, String text); + + void wtf(String TAG, String text); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/Logger.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/Logger.java new file mode 100644 index 0000000..e8f756b --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/Logger.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log; + + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Locale; + +public class Logger { + public static final String NEW_LINE = System.getProperty("line.separator"); + + private static final StringBuilder msFormatBuilder = new StringBuilder(1024); + private static final java.util.Formatter msFormatter = new java.util.Formatter(msFormatBuilder, Locale.US); + + private static final String[] msLogs = new String[BuildConfig.TESTING_BUILD ? 225 : 0]; + private static final String LVL_V = "V"; + private static final String LVL_D = "D"; + private static final String LVL_YELL = "YELL"; + private static final String LVL_I = "I"; + private static final String LVL_W = "W"; + private static final String LVL_E = "E"; + private static final String LVL_WTF = "WTF"; + private static int msLogIndex = 0; + @NonNull + private static LogProvider msLogger = new LogCatLogProvider(); + + private Logger() { + //no instances please. + } + + public static void setLogProvider(@NonNull LogProvider logProvider) { + msLogger = logProvider; + } + + private synchronized static void addLog(String level, String tag, String message) { + if (BuildConfig.TESTING_BUILD) { + msLogs[msLogIndex] = System.currentTimeMillis() + "-" + level + "-[" + tag + "] " + message; + msLogIndex = (msLogIndex + 1) % msLogs.length; + } + } + + private synchronized static void addLog(String level, String tag, String message, Throwable t) { + if (BuildConfig.TESTING_BUILD) { + addLog(level, tag, message); + addLog(level, tag, getStackTrace(t)); + } + } + + @NonNull + public synchronized static ArrayList getAllLogLinesList() { + ArrayList lines = new ArrayList<>(msLogs.length); + if (msLogs.length > 0) { + int index = msLogIndex; + do { + index--; + if (index == -1) index = msLogs.length - 1; + String logLine = msLogs[index]; + if (logLine == null) + break; + lines.add(msLogs[index]); + } + while (index != msLogIndex); + } + return lines; + } + + @NonNull + public synchronized static String getAllLogLines() { + if (BuildConfig.TESTING_BUILD) { + ArrayList lines = getAllLogLinesList(); + //now to build the string + StringBuilder sb = new StringBuilder("Log contains " + lines.size() + " lines:"); + while (lines.size() > 0) { + String line = lines.remove(lines.size() - 1); + sb.append(NEW_LINE); + sb.append(line); + } + return sb.toString(); + } else { + return "Not supported in RELEASE mode!"; + } + } + + public synchronized static void v(String TAG, String text, Object... args) { + if (BuildConfig.DEBUG) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.v(TAG, msg); + addLog(LVL_V, TAG, msg); + } + } + + public synchronized static void v(String TAG, String text, Throwable t) { + if (BuildConfig.DEBUG) { + msLogger.v(TAG, text + NEW_LINE + t); + addLog(LVL_V, TAG, text, t); + } + } + + public synchronized static void d(String TAG, String text) { + if (BuildConfig.TESTING_BUILD) { + msLogger.d(TAG, text); + addLog(LVL_D, TAG, text); + } + } + + public synchronized static void d(String TAG, String text, Object... args) { + if (BuildConfig.TESTING_BUILD) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.d(TAG, msg); + addLog(LVL_D, TAG, msg); + } + } + + public synchronized static void d(String TAG, String text, Throwable t) { + if (BuildConfig.TESTING_BUILD) { + msLogger.d(TAG, text + NEW_LINE + t); + addLog(LVL_D, TAG, text, t); + } + } + + public synchronized static void yell(String TAG, String text, Object... args) { + if (BuildConfig.TESTING_BUILD) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.yell(TAG, msg); + addLog(LVL_YELL, TAG, msg); + } + } + + public synchronized static void i(String TAG, String text, Object... args) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.i(TAG, msg); + addLog(LVL_I, TAG, msg); + } + + public synchronized static void i(String TAG, String text, Throwable t) { + msLogger.i(TAG, text + NEW_LINE + t); + addLog(LVL_I, TAG, text, t); + } + + public synchronized static void w(String TAG, String text, Object... args) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.w(TAG, msg); + addLog(LVL_W, TAG, msg); + } + + public synchronized static void w(String TAG, String text, Throwable t) { + msLogger.w(TAG, text + NEW_LINE + t); + addLog(LVL_W, TAG, text, t); + } + + public synchronized static void e(String TAG, String text, Object... args) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.e(TAG, msg); + addLog(LVL_E, TAG, msg); + } + + //TODO: remove this method + public synchronized static void e(String TAG, String text, Throwable t) { + msLogger.e(TAG, text + NEW_LINE + t); + addLog(LVL_E, TAG, text, t); + } + + public synchronized static void w(String TAG, Throwable e, String text, Object... args) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + msLogger.e(TAG, msg + NEW_LINE + e); + addLog(LVL_E, TAG, msg); + } + + public synchronized static void wtf(String TAG, String text, Object... args) { + String msg = args == null ? text : msFormatter.format(text, args).toString(); + msFormatBuilder.setLength(0); + addLog(LVL_WTF, TAG, msg); + msLogger.wtf(TAG, msg); + } + + public synchronized static void wtf(String TAG, String text, Throwable t) { + addLog(LVL_WTF, TAG, text, t); + msLogger.wtf(TAG, text + NEW_LINE + t); + } + + public static String getStackTrace(Throwable ex) { + StackTraceElement[] stackTrace = ex.getStackTrace(); + StringBuilder sb = new StringBuilder(); + + for (StackTraceElement element : stackTrace) { + sb.append("at ");//this is required for easy Proguard decoding. + sb.append(element.toString()); + sb.append(NEW_LINE); + } + + if (ex.getCause() == null) + return sb.toString(); + else { + ex = ex.getCause(); + String cause = getStackTrace(ex); + sb.append("*** Cause: ").append(ex.getClass().getName()); + sb.append(NEW_LINE); + sb.append("** Message: ").append(ex.getMessage()); + sb.append(NEW_LINE); + sb.append("** Stack track: ").append(cause); + sb.append(NEW_LINE); + return sb.toString(); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/NullLogProvider.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/NullLogProvider.java new file mode 100644 index 0000000..f041d8a --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/log/NullLogProvider.java @@ -0,0 +1,39 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log; + +/** + * Doesn't do anything. For release. + */ +public class NullLogProvider implements LogProvider { + @Override + public void v(String TAG, String text) { + } + + @Override + public void d(String TAG, String text) { + } + + @Override + public void yell(String TAG, String text) { + + } + + @Override + public void i(String TAG, String text) { + + } + + @Override + public void w(String TAG, String text) { + + } + + @Override + public void e(String TAG, String text) { + + } + + @Override + public void wtf(String TAG, String text) { + + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/xml/XmlUtils.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/xml/XmlUtils.java new file mode 100644 index 0000000..5f8aeb6 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/xml/XmlUtils.java @@ -0,0 +1,825 @@ +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.xml; + +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class XmlUtils { + + public static void skipCurrentTag(XmlPullParser parser) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + } + } + + public static int convertValueToList(CharSequence value, String[] options, int defaultValue) { + if (null != value) { + for (int i = 0; i < options.length; i++) { + if (value.equals(options[i])) + return i; + } + } + + return defaultValue; + } + + public static boolean convertValueToBoolean(CharSequence value, boolean defaultValue) { + boolean result = false; + + if (null == value) + return defaultValue; + + if (value.equals("1") + || value.equals("true") + || value.equals("TRUE")) + result = true; + + return result; + } + + public static int convertValueToInt(CharSequence charSeq, int defaultValue) { + if (null == charSeq) + return defaultValue; + + String nm = charSeq.toString(); + + // XXX This code is copied from Integer.decode() so we don't + // have to instantiate an Integer! + + int value; + int sign = 1; + int index = 0; + int len = nm.length(); + int base = 10; + + if ('-' == nm.charAt(0)) { + sign = -1; + index++; + } + + if ('0' == nm.charAt(index)) { + // Quick check for a zero by itself + if (index == (len - 1)) + return 0; + + char c = nm.charAt(index + 1); + + if ('x' == c || 'X' == c) { + index += 2; + base = 16; + } else { + index++; + base = 8; + } + } else if ('#' == nm.charAt(index)) { + index++; + base = 16; + } + + return Integer.parseInt(nm.substring(index), base) * sign; + } + + public static int convertValueToUnsignedInt(String value, int defaultValue) { + if (null == value) + return defaultValue; + + return parseUnsignedIntAttribute(value); + } + + public static int parseUnsignedIntAttribute(CharSequence charSeq) { + String value = charSeq.toString(); + + long bits; + int index = 0; + int len = value.length(); + int base = 10; + + if ('0' == value.charAt(index)) { + // Quick check for zero by itself + if (index == (len - 1)) + return 0; + + char c = value.charAt(index + 1); + + if ('x' == c || 'X' == c) { // check for hex + index += 2; + base = 16; + } else { // check for octal + index++; + base = 8; + } + } else if ('#' == value.charAt(index)) { + index++; + base = 16; + } + + return (int) Long.parseLong(value.substring(index), base); + } + + /** + * Flatten a Map into an output stream as XML. The map can later be + * read back with readMapXml(). + * + * @param val The map to be flattened. + * @param out Where to write the XML data. + * + * @see #writeMapXml(Map, String, XmlSerializer) + * @see #writeListXml + * @see #writeValueXml + * @see #readMapXml + */ +// public static final void writeMapXml(Map val, OutputStream out) +// throws XmlPullParserException, java.io.IOException { +// XmlSerializer serializer = new FastXmlSerializer(); +// serializer.setOutput(out, "utf-8"); +// serializer.startDocument(null, true); +// serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); +// writeMapXml(val, null, serializer); +// serializer.endDocument(); +// } + + /** + * Flatten a List into an output stream as XML. The list can later be + * read back with readListXml(). + * + * @param val The list to be flattened. + * @param out Where to write the XML data. + * @see #writeListXml(List, String, XmlSerializer) + * @see #writeMapXml + * @see #writeValueXml + * @see #readListXml + */ + public static void writeListXml(List val, OutputStream out) + throws XmlPullParserException, java.io.IOException { + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(out, "utf-8"); + serializer.startDocument(null, true); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + writeListXml(val, null, serializer); + serializer.endDocument(); + } + + /** + * Flatten a Map into an XmlSerializer. The map can later be read back + * with readThisMapXml(). + * + * @param val The map to be flattened. + * @param name Name attribute to include with this list's tag, or null for + * none. + * @param out XmlSerializer to write the map into. + * @see #writeListXml + * @see #writeValueXml + * @see #readMapXml + */ + public static void writeMapXml(Map val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + Set s = val.entrySet(); + Iterator i = s.iterator(); + + out.startTag(null, "map"); + if (name != null) { + out.attribute(null, "name", name); + } + + while (i.hasNext()) { + Map.Entry e = (Map.Entry) i.next(); + writeValueXml(e.getValue(), (String) e.getKey(), out); + } + + out.endTag(null, "map"); + } + + /** + * Flatten a List into an XmlSerializer. The list can later be read back + * with readThisListXml(). + * + * @param val The list to be flattened. + * @param name Name attribute to include with this list's tag, or null for + * none. + * @param out XmlSerializer to write the list into. + * @see #writeListXml(List, OutputStream) + * @see #writeMapXml + * @see #writeValueXml + * @see #readListXml + */ + public static void writeListXml(List val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "list"); + if (name != null) { + out.attribute(null, "name", name); + } + + int N = val.size(); + int i = 0; + while (i < N) { + writeValueXml(val.get(i), null, out); + i++; + } + + out.endTag(null, "list"); + } + + public static void writeSetXml(Set val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "set"); + if (name != null) { + out.attribute(null, "name", name); + } + + for (Object v : val) { + writeValueXml(v, null, out); + } + + out.endTag(null, "set"); + } + + /** + * Flatten a byte[] into an XmlSerializer. The list can later be read back + * with readThisByteArrayXml(). + * + * @param val The byte array to be flattened. + * @param name Name attribute to include with this array's tag, or null for + * none. + * @param out XmlSerializer to write the array into. + * @see #writeMapXml + * @see #writeValueXml + */ + public static void writeByteArrayXml(byte[] val, String name, + XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "byte-array"); + if (name != null) { + out.attribute(null, "name", name); + } + + final int N = val.length; + out.attribute(null, "num", Integer.toString(N)); + + StringBuilder sb = new StringBuilder(val.length * 2); + for (byte b : val) { + int h = b >> 4; + sb.append(h >= 10 ? ('a' + h - 10) : ('0' + h)); + h = b & 0xff; + sb.append(h >= 10 ? ('a' + h - 10) : ('0' + h)); + } + + out.text(sb.toString()); + + out.endTag(null, "byte-array"); + } + + /** + * Flatten an int[] into an XmlSerializer. The list can later be read back + * with readThisIntArrayXml(). + * + * @param val The int array to be flattened. + * @param name Name attribute to include with this array's tag, or null for + * none. + * @param out XmlSerializer to write the array into. + * @see #writeMapXml + * @see #writeValueXml + * @see #readThisIntArrayXml + */ + public static void writeIntArrayXml(int[] val, String name, + XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "int-array"); + if (name != null) { + out.attribute(null, "name", name); + } + + final int N = val.length; + out.attribute(null, "num", Integer.toString(N)); + + for (int aVal : val) { + out.startTag(null, "item"); + out.attribute(null, "value", Integer.toString(aVal)); + out.endTag(null, "item"); + } + + out.endTag(null, "int-array"); + } + + /** + * Flatten an object's value into an XmlSerializer. The value can later + * be read back with readThisValueXml(). + *

+ * Currently supported value types are: null, String, Integer, Long, + * Float, Double Boolean, Map, List. + * + * @param v The object to be flattened. + * @param name Name attribute to include with this value's tag, or null + * for none. + * @param out XmlSerializer to write the object into. + * @see #writeMapXml + * @see #writeListXml + * @see #readValueXml + */ + public static void writeValueXml(Object v, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + String typeStr; + if (v == null) { + out.startTag(null, "null"); + if (name != null) { + out.attribute(null, "name", name); + } + out.endTag(null, "null"); + return; + } else if (v instanceof String) { + out.startTag(null, "string"); + if (name != null) { + out.attribute(null, "name", name); + } + out.text(v.toString()); + out.endTag(null, "string"); + return; + } else if (v instanceof Integer) { + typeStr = "int"; + } else if (v instanceof Long) { + typeStr = "long"; + } else if (v instanceof Float) { + typeStr = "float"; + } else if (v instanceof Double) { + typeStr = "double"; + } else if (v instanceof Boolean) { + typeStr = "boolean"; + } else if (v instanceof byte[]) { + writeByteArrayXml((byte[]) v, name, out); + return; + } else if (v instanceof int[]) { + writeIntArrayXml((int[]) v, name, out); + return; + } else if (v instanceof Map) { + writeMapXml((Map) v, name, out); + return; + } else if (v instanceof List) { + writeListXml((List) v, name, out); + return; + } else if (v instanceof Set) { + writeSetXml((Set) v, name, out); + return; + } else if (v instanceof CharSequence) { + // XXX This is to allow us to at least write something if + // we encounter styled text... but it means we will drop all + // of the styling information. :( + out.startTag(null, "string"); + if (name != null) { + out.attribute(null, "name", name); + } + out.text(v.toString()); + out.endTag(null, "string"); + return; + } else { + throw new RuntimeException("writeValueXml: unable to write value " + v); + } + + out.startTag(null, typeStr); + if (name != null) { + out.attribute(null, "name", name); + } + out.attribute(null, "value", v.toString()); + out.endTag(null, typeStr); + } + + /** + * Read a HashMap from an InputStream containing XML. The stream can + * previously have been written by writeMapXml(). + * + * @param in The InputStream from which to read. + * @return HashMap The resulting map. + * @see #readListXml + * @see #readValueXml + * @see #readThisMapXml + * #see #writeMapXml + */ + public static HashMap readMapXml(InputStream in) + throws XmlPullParserException, java.io.IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + return (HashMap) readValueXml(parser, new String[1]); + } + + /** + * Read an ArrayList from an InputStream containing XML. The stream can + * previously have been written by writeListXml(). + * + * @param in The InputStream from which to read. + * @return ArrayList The resulting list. + * @see #readMapXml + * @see #readValueXml + * @see #readThisListXml + * @see #writeListXml + */ + public static ArrayList readListXml(InputStream in) + throws XmlPullParserException, java.io.IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + return (ArrayList) readValueXml(parser, new String[1]); + } + + + /** + * Read a HashSet from an InputStream containing XML. The stream can + * previously have been written by writeSetXml(). + * + * @param in The InputStream from which to read. + * @return HashSet The resulting set. + * @throws XmlPullParserException + * @throws java.io.IOException + * @see #readValueXml + * @see #readThisSetXml + * @see #writeSetXml + */ + public static HashSet readSetXml(InputStream in) + throws XmlPullParserException, java.io.IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(in, null); + return (HashSet) readValueXml(parser, new String[1]); + } + + /** + * Read a HashMap object from an XmlPullParser. The XML data could + * previously have been generated by writeMapXml(). The XmlPullParser + * must be positioned after the tag that begins the map. + * + * @param parser The XmlPullParser from which to read the map data. + * @param endTag Name of the tag that will end the map, usually "map". + * @param name An array of one string, used to return the name attribute + * of the map's tag. + * @return HashMap The newly generated map. + * @see #readMapXml + */ + public static HashMap readThisMapXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + HashMap map = new HashMap<>(); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + Object val = readThisValueXml(parser, name); + if (name[0] != null) { + map.put(name[0], val); + } else { + throw new XmlPullParserException( + "Map value without name attribute: " + parser.getName()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return map; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Read an ArrayList object from an XmlPullParser. The XML data could + * previously have been generated by writeListXml(). The XmlPullParser + * must be positioned after the tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "list". + * @param name An array of one string, used to return the name attribute + * of the list's tag. + * @return HashMap The newly generated list. + * @see #readListXml + */ + public static ArrayList readThisListXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + ArrayList list = new ArrayList<>(); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + Object val = readThisValueXml(parser, name); + list.add(val); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return list; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Read a HashSet object from an XmlPullParser. The XML data could previously + * have been generated by writeSetXml(). The XmlPullParser must be positioned + * after the tag that begins the set. + * + * @param parser The XmlPullParser from which to read the set data. + * @param endTag Name of the tag that will end the set, usually "set". + * @param name An array of one string, used to return the name attribute + * of the set's tag. + * @return HashSet The newly generated set. + * @throws XmlPullParserException + * @throws java.io.IOException + * @see #readSetXml + */ + public static HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + HashSet set = new HashSet<>(); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + Object val = readThisValueXml(parser, name); + set.add(val); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return set; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Read an int[] object from an XmlPullParser. The XML data could + * previously have been generated by writeIntArrayXml(). The XmlPullParser + * must be positioned after the tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "list". + * @param name An array of one string, used to return the name attribute + * of the list's tag. + * @return Returns a newly generated int[]. + * @see #readListXml + */ + public static int[] readThisIntArrayXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + + int num; + try { + num = Integer.parseInt(parser.getAttributeValue(null, "num")); + } catch (NullPointerException e) { + throw new XmlPullParserException( + "Need num attribute in byte-array"); + } catch (NumberFormatException e) { + throw new XmlPullParserException( + "Not a number in num attribute in byte-array"); + } + + int[] array = new int[num]; + int i = 0; + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + try { + array[i] = Integer.parseInt( + parser.getAttributeValue(null, "value")); + } catch (NullPointerException e) { + throw new XmlPullParserException( + "Need value attribute in item"); + } catch (NumberFormatException e) { + throw new XmlPullParserException( + "Not a number in value attribute in item"); + } + } else { + throw new XmlPullParserException( + "Expected item tag at: " + parser.getName()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return array; + } else if (parser.getName().equals("item")) { + i++; + } else { + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + + parser.getName()); + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Read a flattened object from an XmlPullParser. The XML data could + * previously have been written with writeMapXml(), writeListXml(), or + * writeValueXml(). The XmlPullParser must be positioned at the + * tag that defines the value. + * + * @param parser The XmlPullParser from which to read the object. + * @param name An array of one string, used to return the name attribute + * of the value's tag. + * @return Object The newly generated value object. + * @see #readMapXml + * @see #readListXml + * @see #writeValueXml + */ + public static Object readValueXml(XmlPullParser parser, String[] name) + throws XmlPullParserException, java.io.IOException { + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + return readThisValueXml(parser, name); + } else if (eventType == XmlPullParser.END_TAG) { + throw new XmlPullParserException( + "Unexpected end tag at: " + parser.getName()); + } else if (eventType == XmlPullParser.TEXT) { + throw new XmlPullParserException( + "Unexpected text: " + parser.getText()); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Unexpected end of document"); + } + + private static Object readThisValueXml(XmlPullParser parser, String[] name) + throws XmlPullParserException, java.io.IOException { + final String valueName = parser.getAttributeValue(null, "name"); + final String tagName = parser.getName(); + + Object res; + + switch (tagName) { + case "null": + res = null; + break; + case "string": + String value = ""; + int eventType; + while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("string")) { + name[0] = valueName; + return value; + } + throw new XmlPullParserException("Unexpected end tag in : " + parser.getName()); + } else if (eventType == XmlPullParser.TEXT) { + value += parser.getText(); + } else if (eventType == XmlPullParser.START_TAG) { + throw new XmlPullParserException("Unexpected start tag in : " + parser.getName()); + } + } + throw new XmlPullParserException( + "Unexpected end of document in "); + case "int": + res = Integer.parseInt(parser.getAttributeValue(null, "value")); + break; + case "long": + res = Long.valueOf(parser.getAttributeValue(null, "value")); + break; + case "float": + res = Float.valueOf(parser.getAttributeValue(null, "value")); + break; + case "double": + res = Double.valueOf(parser.getAttributeValue(null, "value")); + break; + case "boolean": + res = Boolean.valueOf(parser.getAttributeValue(null, "value")); + break; + case "int-array": + parser.next(); + res = readThisIntArrayXml(parser, "int-array", name); + name[0] = valueName; + return res; + case "map": + parser.next(); + res = readThisMapXml(parser, "map", name); + name[0] = valueName; + return res; + case "list": + parser.next(); + res = readThisListXml(parser, "list", name); + name[0] = valueName; + return res; + case "set": + parser.next(); + res = readThisSetXml(parser, "set", name); + name[0] = valueName; + return res; + default: + throw new XmlPullParserException( + "Unknown tag: " + tagName); + } + + // Skip through to end tag. + int eventType; + while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(tagName)) { + name[0] = valueName; + return res; + } + throw new XmlPullParserException("Unexpected end tag in <" + tagName + ">: " + parser.getName()); + } else if (eventType == XmlPullParser.TEXT) { + throw new XmlPullParserException("Unexpected text in <" + tagName + ">: " + parser.getName()); + } else if (eventType == XmlPullParser.START_TAG) { + throw new XmlPullParserException("Unexpected start tag in <" + tagName + ">: " + parser.getName()); + } + } + throw new XmlPullParserException("Unexpected end of document in <" + tagName + ">"); + } + + public static void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + public static void nextElement(XmlPullParser parser) throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { + ; + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/xml/XmlWriter.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/xml/XmlWriter.java new file mode 100644 index 0000000..a0a0c15 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/extkeyboards/utils/xml/XmlWriter.java @@ -0,0 +1,252 @@ + +/* + * Copyright (c) 2013 Menny Even-Danan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.xml; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InvalidObjectException; +import java.io.Writer; +import java.util.Stack; + +/** + * Makes writing XML much much easier. + * + * @author Henri Yandell + * @author Menny Even Danan - just + * added some features on Henri's initial version + * @version 0.2 + */ +public class XmlWriter { + + private static final String INDENT_STRING = " "; + private final boolean thisIsWriterOwner;// is this instance the owner? + private final Writer writer; // underlying writer + private final int indentingOffset; + private final Stack stack; // of xml entity names + private final StringBuffer attrs; // current attribute string + private boolean empty; // is the current node empty + private boolean justWroteText; + private boolean closed; // is the current node closed... + + /** + * Create an XmlWriter on top of an existing java.io.Writer. + * + * @throws IOException + */ + public XmlWriter(Writer writer, boolean takeOwnership, int indentingOffset, boolean addXmlPrefix) + throws IOException { + thisIsWriterOwner = takeOwnership; + this.indentingOffset = indentingOffset; + this.writer = writer; + this.closed = true; + this.stack = new Stack(); + this.attrs = new StringBuffer(); + if (addXmlPrefix) + this.writer.write("\n"); + } + + public XmlWriter(File outputFile) throws IOException { + this(new FileWriter(outputFile), true, 0, true); + } + + /** + * Begin to output an entity. + */ + public XmlWriter writeEntity(String name) throws IOException { + closeOpeningTag(true); + this.closed = false; + for (int tabIndex = 0; tabIndex < stack.size() + indentingOffset; tabIndex++) + this.writer.write(INDENT_STRING); + this.writer.write("<"); + this.writer.write(name); + stack.add(name); + this.empty = true; + this.justWroteText = false; + return this; + } + + // close off the opening tag + private void closeOpeningTag(final boolean newLine) throws IOException { + if (!this.closed) { + writeAttributes(); + this.closed = true; + this.writer.write(">"); + if (newLine) + this.writer.write("\n"); + } + } + + // write out all current attributes + private void writeAttributes() throws IOException { + this.writer.write(this.attrs.toString()); + this.attrs.setLength(0); + this.empty = false; + } + + /** + * Write an attribute out for the current entity. Any xml characters in the + * value are escaped. Currently it does not actually throw the exception, + * but the api is set that way for future changes. + * + * @param String name of attribute. + * @param String value of attribute. + */ + public XmlWriter writeAttribute(String attr, String value) { + this.attrs.append(" "); + this.attrs.append(attr); + this.attrs.append("=\""); + this.attrs.append(escapeXml(value)); + this.attrs.append("\""); + return this; + } + + /** + * End the current entity. This will throw an exception if it is called when + * there is not a currently open entity. + * + * @throws IOException + */ + public XmlWriter endEntity() throws IOException { + if (this.stack.empty()) { + throw new InvalidObjectException("Called endEntity too many times. "); + } + String name = this.stack.pop(); + if (name != null) { + if (this.empty) { + writeAttributes(); + this.writer.write("/>\n"); + } else { + if (!this.justWroteText) { + for (int tabIndex = 0; tabIndex < stack.size() + indentingOffset; tabIndex++) + this.writer.write(INDENT_STRING); + } + this.writer.write("\n"); + } + this.empty = false; + this.closed = true; + this.justWroteText = false; + } + return this; + } + + /** + * Close this writer. It does not close the underlying writer, but does + * throw an exception if there are as yet unclosed tags. + * + * @throws IOException + */ + public void close() throws IOException { + if (!this.stack.empty()) { + throw new InvalidObjectException("Tags are not all closed. " + + "Possibly, " + this.stack.pop() + " is unclosed. "); + } + if (thisIsWriterOwner) { + this.writer.flush(); + this.writer.close(); + } + } + + /** + * Output body text. Any xml characters are escaped. + */ + public XmlWriter writeText(String text) throws IOException { + closeOpeningTag(false); + this.empty = false; + this.justWroteText = true; + this.writer.write(escapeXml(text)); + return this; + } + + // Static functions lifted from generationjava helper classes + // to make the jar smaller. + + // from XmlW + static public String escapeXml(String str) { + str = replaceString(str, "&", "&"); + str = replaceString(str, "<", "<"); + str = replaceString(str, ">", ">"); + str = replaceString(str, "\"", """); + str = replaceString(str, "'", "'"); + return str; + } + + // from StringW + static public String replaceString(String text, String repl, String with) { + return replaceString(text, repl, with, -1); + } + + /** + * Replace a string with another string inside a larger string, for the + * first n values of the search string. + * + * @param text String to do search and replace in + * @param repl String to search for + * @param with String to replace with + * @param max int values to replace + * @return String with n values replacEd + */ + static public String replaceString(String text, String repl, String with, int max) { + if (text == null) { + return null; + } + + StringBuffer buffer = new StringBuffer(text.length()); + int start = 0; + int end = 0; + while ((end = text.indexOf(repl, start)) != -1) { + buffer.append(text.substring(start, end)).append(with); + start = end + repl.length(); + + if (--max == 0) { + break; + } + } + buffer.append(text.substring(start)); + + return buffer.toString(); + } + // + // static public void test1() throws WritingException { + // Writer writer = new java.io.StringWriter(); + // XmlWriter xmlwriter = new XmlWriter(writer); + // xmlwriter.writeEntity("person").writeAttribute("name", + // "fred").writeAttribute("age", + // "12").writeEntity("phone").writeText("4254343").endEntity().writeEntity("bob").endEntity().endEntity(); + // xmlwriter.close(); + // System.err.println(writer.toString()); + // } + // static public void test2() throws WritingException { + // Writer writer = new java.io.StringWriter(); + // XmlWriter xmlwriter = new XmlWriter(writer); + // xmlwriter.writeEntity("person"); + // xmlwriter.writeAttribute("name", "fred"); + // xmlwriter.writeAttribute("age", "12"); + // xmlwriter.writeEntity("phone"); + // xmlwriter.writeText("4254343"); + // xmlwriter.endEntity(); + // xmlwriter.writeEntity("bob"); + // xmlwriter.endEntity(); + // xmlwriter.endEntity(); + // xmlwriter.close(); + // System.err.println(writer.toString()); + // } + +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/CheckedSource.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/CheckedSource.java new file mode 100644 index 0000000..62a32ca --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/CheckedSource.java @@ -0,0 +1,14 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards; + +import java.util.List; + +public interface CheckedSource { + List getItems(); + + interface CheckedItem { + long getId(); + String getTitle(); + void onClick(boolean checked); + boolean getChecked(); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/KeyboardInfoAdapter.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/KeyboardInfoAdapter.java new file mode 100644 index 0000000..efc9f8c --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/KeyboardInfoAdapter.java @@ -0,0 +1,62 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards; + +import android.content.Context; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardInfo; + +import java.util.ArrayList; +import java.util.List; + +public class KeyboardInfoAdapter implements CheckedSource { + private final Context mContext; + private final List mInfos; + + public KeyboardInfoAdapter(Context context) { + mContext = context; + mInfos = ResKeyboardInfo.getAllKeyboardInfos(context); + } + + @Override + public List getItems() { + List result = new ArrayList<>(); + + int counter = 99; + + for (KeyboardInfo info : mInfos) { + int finalCounter = counter++; + + CheckedItem item = new CheckedItem() { + private final KeyboardInfo mInfo = info; + private final int mCounter = finalCounter; + + @Override + public long getId() { + return mCounter; + } + + @Override + public String getTitle() { + return mInfo.getLangName(); + } + + @Override + public void onClick(boolean checked) { + if (mInfo.isEnabled() == checked) { + return; + } + + mInfo.setEnabled(checked); + ResKeyboardInfo.updateAllKeyboardInfos(mContext, mInfos); + } + + @Override + public boolean getChecked() { + return mInfo.isEnabled(); + } + }; + + result.add(item); + } + + return result; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/ResKeyboardFactory.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/ResKeyboardFactory.java new file mode 100644 index 0000000..b7b6a60 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/ResKeyboardFactory.java @@ -0,0 +1,139 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.text.Layout; +import android.util.Log; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardBuilder; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardFactory; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardInfo; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardView; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; +import com.liskovsoft.leankeyboard.utils.TextDrawable; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class ResKeyboardFactory implements KeyboardFactory { + private static final String TAG = ResKeyboardFactory.class.getSimpleName(); + private final Context mContext; + private Map mCachedSpace; + + public ResKeyboardFactory(Context ctx) { + mContext = ctx; + mCachedSpace = new HashMap<>(); + } + + @Override + public List getAllAvailableKeyboards(Context context) { + List result = new ArrayList<>(); + List infos = ResKeyboardInfo.getAllKeyboardInfos(context); + + for (final KeyboardInfo info : infos) { + if (info.isEnabled()) { + result.add(createKeyboard(info)); + } + } + + // at least one kbd should be enabled + if (result.isEmpty()) { + KeyboardInfo defaultKbd = findDefaultKeyboard(infos); + result.add(createKeyboard(defaultKbd)); + defaultKbd.setEnabled(true); + //ResKeyboardInfo.updateAllKeyboardInfos(mContext, infos); + } + + return result; + } + + private KeyboardInfo findDefaultKeyboard(List infos) { + KeyboardInfo defaultKeyboard = infos.get(0); + + if (LeanKeyPreferences.instance(mContext).getAutodetectLayout()) { + Locale defaultLocale = Locale.getDefault(); + String lang = defaultLocale.getLanguage(); + + for (final KeyboardInfo info : infos) { + if (info.getLangCode().startsWith(lang)) { + defaultKeyboard = info; + break; + } + } + } + + return defaultKeyboard; + } + + /** + * NOTE: create keyboard from xml data + */ + private KeyboardBuilder createKeyboard(final KeyboardInfo info) { + return new KeyboardBuilder() { + private final String langCode = info.getLangCode(); + + @Override + public Keyboard createAbcKeyboard() { + String prefix = info.isAzerty() ? "azerty_" : "qwerty_"; + int kbResId = mContext.getResources().getIdentifier(prefix + langCode, "xml", mContext.getPackageName()); + Keyboard keyboard = new Keyboard(mContext, kbResId); + Log.d(TAG, "Creating keyboard... " + info.getLangName()); + return localizeKeys(keyboard, info); + } + + @Override + public Keyboard createSymKeyboard() { + Keyboard keyboard = new Keyboard(mContext, R.xml.sym_en_us); + return localizeKeys(keyboard, info); + } + + @Override + public Keyboard createNumKeyboard() { + return new Keyboard(mContext, R.xml.number); + } + }; + } + + @Override + public boolean needUpdate() { + return ResKeyboardInfo.needUpdate(); + } + + private Keyboard localizeKeys(Keyboard keyboard, KeyboardInfo info) { + List keys = keyboard.getKeys(); + + for (Key key : keys) { + if (key.codes[0] == LeanbackKeyboardView.ASCII_SPACE) { + localizeSpace(key, info); + break; + } + } + + return keyboard; + } + + private void localizeSpace(Key key, KeyboardInfo info) { + if (mCachedSpace.containsKey(info.getLangCode())) { + key.icon = mCachedSpace.get(info.getLangCode()); + return; + } + + TextDrawable drawable = new TextDrawable(mContext, key.icon); + drawable.setText(info.getLangName()); + drawable.setTextAlign(Layout.Alignment.ALIGN_CENTER); + //Customize text size and color + drawable.setTextColor(Color.WHITE); + drawable.setTextSizeFactor(0.3f); + drawable.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD); + key.icon = drawable; + + mCachedSpace.put(info.getLangCode(), drawable); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/ResKeyboardInfo.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/ResKeyboardInfo.java new file mode 100644 index 0000000..a47b224 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/keyboards/intkeyboards/ResKeyboardInfo.java @@ -0,0 +1,111 @@ +package com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import androidx.annotation.NonNull; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardInfo; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.ArrayList; +import java.util.List; + +public class ResKeyboardInfo implements KeyboardInfo { + private static boolean sNeedUpdate; + private boolean mEnabled; + private String mLangCode; + private String mLangName; + private boolean mIsAzerty; + + public static List getAllKeyboardInfos(Context ctx) { + List result = new ArrayList<>(); + String[] langs = ctx.getResources().getStringArray(R.array.additional_languages); + for (final String langPair : langs) { + String[] pairs = langPair.split("\\|"); + final String langName = pairs[0]; + final String langCode = pairs[1]; + final boolean isAzerty = pairs.length >= 3 && "azerty".equals(pairs[2]); + KeyboardInfo info = new ResKeyboardInfo(); + info.setLangName(langName); + info.setLangCode(langCode); + info.setIsAzerty(isAzerty); + // sync with prefs + syncWithPrefs(ctx, info); + result.add(info); + } + sNeedUpdate = false; + return result; + } + + public static void updateAllKeyboardInfos(Context ctx, List infos) { + for (KeyboardInfo info : infos) { + // update prefs + updatePrefs(ctx, info); + } + sNeedUpdate = true; + } + + private static void syncWithPrefs(Context ctx, KeyboardInfo info) { + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx); + final boolean kbdEnabled = sharedPreferences.getBoolean(info.toString(), false); + info.setEnabled(kbdEnabled); + } + + private static void updatePrefs(Context ctx, KeyboardInfo info) { + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx); + + final SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putBoolean(info.toString(), info.isEnabled()); + editor.apply(); + } + + public static boolean needUpdate() { + return sNeedUpdate; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public String getLangCode() { + return mLangCode; + } + + @Override + public String getLangName() { + return mLangName; + } + + @Override + public void setLangName(String langName) { + mLangName = langName; + } + + @Override + public void setLangCode(String langCode) { + mLangCode = langCode; + } + + @Override + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + @Override + public boolean isAzerty() { + return mIsAzerty; + } + + @Override + public void setIsAzerty(boolean isAzerty) { + mIsAzerty = isAzerty; + } + + @NonNull + @Override + public String toString() { + return String.format("{Name: %s, Code: %s, IsAzerty: %b}", mLangName, mLangCode, mIsAzerty); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/resize/KeyboardWrapper.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/resize/KeyboardWrapper.java new file mode 100644 index 0000000..bd861c1 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/resize/KeyboardWrapper.java @@ -0,0 +1,99 @@ +package com.liskovsoft.leankeyboard.addons.resize; + +import android.content.Context; +import android.inputmethodservice.Keyboard; +import androidx.annotation.Nullable; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardContainer; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.List; + +public class KeyboardWrapper extends Keyboard { + private Keyboard mKeyboard; + private int mHeight = -1; + private float mHeightFactor = 1.0f; + private float mWidthFactor = 1.0f; + + public KeyboardWrapper(Context context, int xmlLayoutResId) { + super(context, xmlLayoutResId); + } + + public KeyboardWrapper(Context context, int xmlLayoutResId, int modeId, int width, int height) { + super(context, xmlLayoutResId, modeId, width, height); + } + + public KeyboardWrapper(Context context, int xmlLayoutResId, int modeId) { + super(context, xmlLayoutResId, modeId); + } + + public KeyboardWrapper(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding) { + super(context, layoutTemplateResId, characters, columns, horizontalPadding); + } + + public static KeyboardWrapper from(Keyboard keyboard, Context context) { + KeyboardWrapper wrapper = new KeyboardWrapper(context, R.xml.empty_kbd); + wrapper.mKeyboard = keyboard; + + return wrapper; + } + + @Override + public List getKeys() { + return mKeyboard.getKeys(); + } + + @Override + public List getModifierKeys() { + return mKeyboard.getModifierKeys(); + } + + @Override + public int getHeight() { + return (int)(mKeyboard.getHeight() * mHeightFactor); + } + + @Override + public int getMinWidth() { + return (int)(mKeyboard.getMinWidth() * mWidthFactor); + } + + @Override + public boolean setShifted(boolean shiftState) { + return mKeyboard.setShifted(shiftState); + } + + @Override + public boolean isShifted() { + return mKeyboard.isShifted(); + } + + @Override + public int getShiftKeyIndex() { + return mKeyboard.getShiftKeyIndex(); + } + + @Override + public int[] getNearestKeys(int x, int y) { + return mKeyboard.getNearestKeys(x, y); + } + + public void setHeightFactor(float factor) { + mHeightFactor = factor; + } + + public void setWidthFactor(float factor) { + mWidthFactor = factor; + } + + /** + * Wrapper fix: {@link LeanbackKeyboardContainer#onModeChangeClick} + */ + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof Keyboard) { + return mKeyboard.equals(obj); + } + + return false; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/resize/ResizeableLeanbackKeyboardView.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/resize/ResizeableLeanbackKeyboardView.java new file mode 100644 index 0000000..3555426 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/resize/ResizeableLeanbackKeyboardView.java @@ -0,0 +1,76 @@ +package com.liskovsoft.leankeyboard.addons.resize; + +import android.content.Context; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.util.AttributeSet; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardView; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; + +import java.util.List; + +public class ResizeableLeanbackKeyboardView extends LeanbackKeyboardView { + private final LeanKeyPreferences mPrefs; + private final int mKeyTextSizeOrigin; + private final int mModeChangeTextSizeOrigin; + private final float mSizeFactor = 1.3f; + private int mKeyOriginWidth; + + public ResizeableLeanbackKeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + mPrefs = LeanKeyPreferences.instance(getContext()); + mKeyTextSizeOrigin = mKeyTextSize; + mModeChangeTextSizeOrigin = mModeChangeTextSize; + } + + @Override + public void setKeyboard(Keyboard keyboard) { + if (mPrefs.getEnlargeKeyboard()) { + mKeyTextSize = (int) (mKeyTextSizeOrigin * mSizeFactor); + mModeChangeTextSize = (int) (mModeChangeTextSizeOrigin * mSizeFactor); + + keyboard = updateKeyboard(keyboard); + } else { + mKeyTextSize = mKeyTextSizeOrigin; + mModeChangeTextSize = mModeChangeTextSizeOrigin; + } + + mPaint.setTextSize(mKeyTextSize); + + super.setKeyboard(keyboard); + } + + private Keyboard updateKeyboard(Keyboard keyboard) { + List keys = keyboard.getKeys(); + + if (isNotSizedYet(keys.get(0))) { + for (Key key : keys) { + key.width *= mSizeFactor; + key.height *= mSizeFactor; + key.gap *= mSizeFactor; + key.x *= mSizeFactor; + key.y *= mSizeFactor; + } + } + + KeyboardWrapper wrapper = KeyboardWrapper.from(keyboard, getContext()); + wrapper.setHeightFactor(mSizeFactor); + wrapper.setWidthFactor(mSizeFactor); + + return wrapper; + } + + private boolean isNotSizedYet(Key key) { + boolean result = false; + + if (mKeyOriginWidth == 0) { + mKeyOriginWidth = key.width; + } + + if (mKeyOriginWidth == key.width) { + result = true; + } + + return result; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/theme/ThemeManager.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/theme/ThemeManager.java new file mode 100644 index 0000000..234170c --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/theme/ThemeManager.java @@ -0,0 +1,159 @@ +package com.liskovsoft.leankeyboard.addons.theme; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import androidx.core.content.ContextCompat; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardView; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; +import com.liskovsoft.leankeykeyboard.R; + +public class ThemeManager { + private static final String TAG = ThemeManager.class.getSimpleName(); + private final Context mContext; + private final RelativeLayout mRootView; + private final LeanKeyPreferences mPrefs; + + public ThemeManager(Context context, RelativeLayout rootView) { + mContext = context; + mRootView = rootView; + mPrefs = LeanKeyPreferences.instance(mContext); + } + + public void updateKeyboardTheme() { + String currentThemeId = mPrefs.getCurrentTheme(); + + if (LeanKeyPreferences.THEME_DEFAULT.equals(currentThemeId)) { + applyKeyboardColors( + R.color.keyboard_background, + R.color.candidate_background, + R.color.enter_key_font_color, + R.color.key_text_default + ); + applyShiftDrawable(-1); + } else { + applyForTheme((String themeId) -> { + Resources resources = mContext.getResources(); + int keyboardBackgroundResId = resources.getIdentifier("keyboard_background_" + themeId.toLowerCase(), "color", mContext.getPackageName()); + int candidateBackgroundResId = resources.getIdentifier("candidate_background_" + themeId.toLowerCase(), "color", mContext.getPackageName()); + int enterFontColorResId = resources.getIdentifier("enter_key_font_color_" + themeId.toLowerCase(), "color", mContext.getPackageName()); + int keyTextColorResId = resources.getIdentifier("key_text_default_" + themeId.toLowerCase(), "color", mContext.getPackageName()); + + applyKeyboardColors( + keyboardBackgroundResId, + candidateBackgroundResId, + enterFontColorResId, + keyTextColorResId + ); + + int shiftLockOnResId = resources.getIdentifier("ic_ime_shift_lock_on_" + themeId.toLowerCase(), "drawable", mContext.getPackageName()); + + applyShiftDrawable(shiftLockOnResId); + }); + } + } + + public void updateSuggestionsTheme() { + String currentTheme = mPrefs.getCurrentTheme(); + + if (LeanKeyPreferences.THEME_DEFAULT.equals(currentTheme)) { + applySuggestionsColors( + R.color.candidate_font_color + ); + } else { + applyForTheme((String themeId) -> { + Resources resources = mContext.getResources(); + int candidateFontColorResId = resources.getIdentifier("candidate_font_color_" + themeId.toLowerCase(), "color", mContext.getPackageName()); + applySuggestionsColors(candidateFontColorResId); + }); + } + } + + private void applyKeyboardColors( + int keyboardBackground, + int candidateBackground, + int enterFontColor, + int keyTextColor) { + + RelativeLayout rootLayout = mRootView.findViewById(R.id.root_ime); + + if (rootLayout != null) { + rootLayout.setBackgroundColor(ContextCompat.getColor(mContext, keyboardBackground)); + } + + View candidateLayout = mRootView.findViewById(R.id.candidate_background); + + if (candidateLayout != null) { + candidateLayout.setBackgroundColor(ContextCompat.getColor(mContext, candidateBackground)); + } + + Button enterButton = mRootView.findViewById(R.id.enter); + + if (enterButton != null) { + enterButton.setTextColor(ContextCompat.getColor(mContext, enterFontColor)); + } + + LeanbackKeyboardView keyboardView = mRootView.findViewById(R.id.main_keyboard); + + if (keyboardView != null) { + keyboardView.setKeyTextColor(ContextCompat.getColor(mContext, keyTextColor)); + } + } + + private void applySuggestionsColors(int candidateFontColor) { + LinearLayout suggestions = mRootView.findViewById(R.id.suggestions); + + if (suggestions != null) { + int childCount = suggestions.getChildCount(); + + Log.d(TAG, "Number of suggestions: " + childCount); + + for (int i = 0; i < childCount; i++) { + View child = suggestions.getChildAt(i); + + Button candidateButton = child.findViewById(R.id.text); + + if (candidateButton != null) { + candidateButton.setTextColor(ContextCompat.getColor(mContext, candidateFontColor)); + } + } + } + } + + private void applyShiftDrawable(int resId) { + LeanbackKeyboardView keyboardView = mRootView.findViewById(R.id.main_keyboard); + + if (keyboardView != null && resId > 0) { + Drawable drawable = ContextCompat.getDrawable(mContext, resId); + + keyboardView.setCapsLockDrawable(drawable); + } + } + + private void applyForTheme(ThemeCallback callback) { + String currentThemeId = mPrefs.getCurrentTheme(); + Resources resources = mContext.getResources(); + String[] themes = resources.getStringArray(R.array.keyboard_themes); + + for (String theme : themes) { + String[] split = theme.split("\\|"); + String themeName = split[0]; + String themeId = split[1]; + + if (currentThemeId.equals(themeId)) { + callback.onThemeFound(themeId); + + break; + } + } + } + + private interface ThemeCallback { + void onThemeFound(String themeId); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/ActivityListener.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/ActivityListener.java new file mode 100644 index 0000000..fb16cf1 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/ActivityListener.java @@ -0,0 +1,7 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +import android.content.Intent; + +interface ActivityListener { + void onActivityResult(int requestCode, int resultCode, Intent data); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerCallback.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerCallback.java new file mode 100644 index 0000000..95721e7 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerCallback.java @@ -0,0 +1,5 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +public interface RecognizerCallback { + void openSearchPage(String searchText); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerIntentActivity.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerIntentActivity.java new file mode 100644 index 0000000..b9ba412 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerIntentActivity.java @@ -0,0 +1,29 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +public class RecognizerIntentActivity extends AppCompatActivity { + public static RecognizerCallback sCallback; + private VoiceSearchBridge mBridge; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mBridge = new VoiceSearchBridge(this, searchText -> sCallback.openSearchPage(searchText)); + + mBridge.displaySpeechRecognizers(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + mBridge.onActivityResult(requestCode, resultCode, data); + + finish(); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerIntentWrapper.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerIntentWrapper.java new file mode 100644 index 0000000..eb3f896 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/RecognizerIntentWrapper.java @@ -0,0 +1,26 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +import android.content.Context; +import android.content.Intent; + +public class RecognizerIntentWrapper { + private final Context mContext; + private RecognizerCallback mCallback; + + public RecognizerIntentWrapper(Context context) { + mContext = context; + } + + public void setListener(RecognizerCallback callback) { + mCallback = callback; + } + + public void startListening() { + if (mCallback != null) { + RecognizerIntentActivity.sCallback = mCallback; + Intent intent = new Intent(mContext, RecognizerIntentActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/SearchCallback.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/SearchCallback.java new file mode 100644 index 0000000..e35e430 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/SearchCallback.java @@ -0,0 +1,5 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +interface SearchCallback { + void openSearchPage(String searchText); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/SystemVoiceDialog.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/SystemVoiceDialog.java new file mode 100644 index 0000000..e7defc4 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/SystemVoiceDialog.java @@ -0,0 +1,45 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.speech.RecognizerIntent; + +import java.util.List; + +class SystemVoiceDialog implements VoiceDialog, ActivityListener { + private static final int SPEECH_REQUEST_CODE = 11; + private final Activity mActivity; + private final SearchCallback mCallback; + + public SystemVoiceDialog(Activity activity, SearchCallback callback) { + mActivity = activity; + mCallback = callback; + } + + public boolean displaySpeechRecognizer() { + Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + try { + mActivity.startActivityForResult(intent, SPEECH_REQUEST_CODE); + } catch (ActivityNotFoundException e) { + e.printStackTrace(); + return false; + } + + return true; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + // got speech-to-text result, switch to the search page + if (requestCode == SPEECH_REQUEST_CODE && resultCode == -1) { + List results = data.getStringArrayListExtra( + RecognizerIntent.EXTRA_RESULTS); + if (results != null && results.size() > 0) { + mCallback.openSearchPage(results.get(0)); + } + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceDialog.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceDialog.java new file mode 100644 index 0000000..2e0bee7 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceDialog.java @@ -0,0 +1,5 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +interface VoiceDialog { + boolean displaySpeechRecognizer(); +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceOverlayDialog.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceOverlayDialog.java new file mode 100644 index 0000000..2a3eaf1 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceOverlayDialog.java @@ -0,0 +1,67 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +import androidx.appcompat.app.AppCompatActivity; +import com.algolia.instantsearch.voice.VoiceSpeechRecognizer; +import com.algolia.instantsearch.voice.ui.Voice; +import com.algolia.instantsearch.voice.ui.VoiceInputDialogFragment; +import com.algolia.instantsearch.voice.ui.VoicePermissionDialogFragment; + +class VoiceOverlayDialog implements VoiceDialog, VoiceSpeechRecognizer.ResultsListener { + private final AppCompatActivity mActivity; + private final SearchCallback mCallback; + + private enum Tag { + Permission, + Voice + } + + public VoiceOverlayDialog(AppCompatActivity activity, SearchCallback callback) { + mActivity = activity; + mCallback = callback; + } + + @Override + public boolean displaySpeechRecognizer() { + if (!Voice.isRecordAudioPermissionGranted(mActivity)) { + new VoicePermissionDialogFragment() + .show(mActivity.getSupportFragmentManager(), Tag.Permission.name()); + } else { + showVoiceDialog(); + } + + return true; + } + + private void showVoiceDialog() { + VoicePermissionDialogFragment dialog = getPermissionDialog(); + + if (dialog != null) { + dialog.dismiss(); + } + + VoiceInputDialogFragment voiceDialog = getVoiceDialog(); + + if (voiceDialog == null) { + voiceDialog = new VoiceInputDialogFragment(); + } else { + voiceDialog.dismiss(); + } + + voiceDialog.show(mActivity.getSupportFragmentManager(), Tag.Voice.name()); + } + + private VoicePermissionDialogFragment getPermissionDialog() { + return (VoicePermissionDialogFragment) mActivity.getSupportFragmentManager().findFragmentByTag(Tag.Permission.name()); + } + + private VoiceInputDialogFragment getVoiceDialog() { + return (VoiceInputDialogFragment) mActivity.getSupportFragmentManager().findFragmentByTag(Tag.Voice.name()); + } + + @Override + public void onResults(String[] strings) { + if (strings.length > 0) { + mCallback.openSearchPage(strings[0]); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceSearchBridge.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceSearchBridge.java new file mode 100644 index 0000000..dc2ecf3 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/addons/voice/VoiceSearchBridge.java @@ -0,0 +1,43 @@ +package com.liskovsoft.leankeyboard.addons.voice; + +import android.content.Intent; +import androidx.appcompat.app.AppCompatActivity; + +import java.util.ArrayList; + +class VoiceSearchBridge implements SearchCallback { + private final ArrayList mDialogs; + private final AppCompatActivity mActivity; + private final RecognizerCallback mCallback; + + public VoiceSearchBridge(AppCompatActivity activity, RecognizerCallback callback) { + mActivity = activity; + mCallback = callback; + mDialogs = new ArrayList<>(); + mDialogs.add(new SystemVoiceDialog(activity, this)); + mDialogs.add(new VoiceOverlayDialog(activity, this)); + } + + public void displaySpeechRecognizers() { + for (VoiceDialog dialog : mDialogs) { + if (dialog.displaySpeechRecognizer()) { // fist successful attempt is used + break; + } + } + } + + public void onActivityResult(int requestCode, int resultCode, Intent data) { + for (VoiceDialog dialog : mDialogs) { + if (dialog instanceof ActivityListener) { + ((ActivityListener) dialog).onActivityResult(requestCode, resultCode, data); + } + } + } + + @Override + public void openSearchPage(String searchText) { + if (mCallback != null) { + mCallback.openSearchPage(searchText); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/BaseSettingsFragment.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/BaseSettingsFragment.java new file mode 100644 index 0000000..4f3b8a3 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/BaseSettingsFragment.java @@ -0,0 +1,190 @@ +package com.liskovsoft.leankeyboard.fragments.settings; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.leanback.app.GuidedStepSupportFragment; +import androidx.leanback.widget.GuidedAction; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class BaseSettingsFragment extends GuidedStepSupportFragment { + private Map mCheckedActions = new LinkedHashMap<>(); + private Map mNextActions = new LinkedHashMap<>(); + private long mId; + + protected interface OnChecked { + void onChecked(boolean checked); + } + + protected interface GetChecked { + boolean getChecked(); + } + + protected interface OnClick { + void onClick(); + } + + // Radio action + + protected void addRadioAction(int titleResId, int descResId, GetChecked getChecked, OnChecked onChecked) { + addRadioAction(getString(titleResId), getString(descResId), getChecked, onChecked); + } + + protected void addRadioAction(int titleRedId, GetChecked getChecked, OnChecked onChecked) { + addRadioAction(getString(titleRedId), getChecked, onChecked); + } + + protected void addRadioAction(String title, GetChecked getChecked, OnChecked onChecked) { + mCheckedActions.put(mId++, new RadioAction(title, getChecked, onChecked)); + } + + protected void addRadioAction(String title, String desc, GetChecked getChecked, OnChecked onChecked) { + mCheckedActions.put(mId++, new RadioAction(title, desc, getChecked, onChecked)); + } + + // Checked action + + protected void addCheckedAction(int titleResId, int descResId, GetChecked getChecked, OnChecked onChecked) { + addCheckedAction(getString(titleResId), getString(descResId), getChecked, onChecked); + } + + protected void addCheckedAction(int titleRedId, GetChecked getChecked, OnChecked onChecked) { + addCheckedAction(getString(titleRedId), getChecked, onChecked); + } + + protected void addCheckedAction(String title, GetChecked getChecked, OnChecked onChecked) { + mCheckedActions.put(mId++, new CheckedAction(title, getChecked, onChecked)); + } + + protected void addCheckedAction(String title, String desc, GetChecked getChecked, OnChecked onChecked) { + mCheckedActions.put(mId++, new CheckedAction(title, desc, getChecked, onChecked)); + } + + // Nested action + + protected void addNextAction(int resId, OnClick onClick) { + mNextActions.put(mId++, new NextAction(resId, onClick)); + } + + @Override + public void onCreateActions(@NonNull List actions, Bundle savedInstanceState) { + for (long id : mCheckedActions.keySet()) { + addCheckedItem(id, mCheckedActions.get(id), actions); + } + + for (long id : mNextActions.keySet()) { + addNextItem(id, mNextActions.get(id), actions); + } + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + CheckedAction checkedAction = mCheckedActions.get(action.getId()); + + if (checkedAction != null) { + checkedAction.onChecked(action.isChecked()); + } + + NextAction nextAction = mNextActions.get(action.getId()); + + if (nextAction != null) { + nextAction.onClick(); + } + } + + private void addNextItem(long id, NextAction nextAction, List actions) { + GuidedAction action = new GuidedAction.Builder(getActivity()) + .id(id) + .hasNext(true) + .title(nextAction.getResId()).build(); + actions.add(action); + } + + private void addCheckedItem(long id, CheckedAction checkedAction, List actions) { + GuidedAction action = new GuidedAction.Builder(getActivity()) + .checked(checkedAction.isChecked()) + .checkSetId(checkedAction.getItemTypeId()) + .id(id) + .title(checkedAction.getTitle()) + .build(); + + if (checkedAction.getDesc() != null) { + action.setDescription(checkedAction.getDesc()); + } + + actions.add(action); + } + + private static class RadioAction extends CheckedAction { + public RadioAction(String title, GetChecked getChecked, OnChecked onChecked) { + super(title, getChecked, onChecked); + } + + public RadioAction(String title, String desc, GetChecked getChecked, OnChecked onChecked) { + super(title, desc, getChecked, onChecked); + } + + @Override + public int getItemTypeId() { + return GuidedAction.DEFAULT_CHECK_SET_ID; + } + } + + private static class CheckedAction { + private final String mDesc; + private final GetChecked mGetChecked; + private final OnChecked mOnChecked; + private final String mTitle; + + public CheckedAction(String title, GetChecked getChecked, OnChecked onChecked) { + this(title, null, getChecked, onChecked); + } + + public CheckedAction(String title, String desc, GetChecked getChecked, OnChecked onChecked) { + mTitle = title; + mDesc = desc; + mGetChecked = getChecked; + mOnChecked = onChecked; + } + + public String getTitle() { + return mTitle; + } + + public String getDesc() { + return mDesc; + } + + public boolean isChecked() { + return mGetChecked.getChecked(); + } + + public void onChecked(boolean checked) { + mOnChecked.onChecked(checked); + } + + public int getItemTypeId() { + return GuidedAction.CHECKBOX_CHECK_SET_ID; + } + } + + private static class NextAction { + private final int mResId; + private final OnClick mOnClick; + + public NextAction(int resId, OnClick onClick) { + mResId = resId; + mOnClick = onClick; + } + + public int getResId() { + return mResId; + } + + public void onClick() { + mOnClick.onClick(); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbLayoutFragment.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbLayoutFragment.java new file mode 100644 index 0000000..dcaa3ff --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbLayoutFragment.java @@ -0,0 +1,43 @@ +package com.liskovsoft.leankeyboard.fragments.settings; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.leanback.widget.GuidanceStylist.Guidance; +import com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards.KeyboardInfoAdapter; +import com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards.CheckedSource; +import com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards.CheckedSource.CheckedItem; +import com.liskovsoft.leankeykeyboard.R; + +public class KbLayoutFragment extends BaseSettingsFragment { + @Override + public void onAttach(Context context) { + super.onAttach(context); + + initCheckedItems(); + } + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getActivity().getResources().getString(R.string.kb_layout); + String desc = getActivity().getResources().getString(R.string.kb_layout_desc); + Drawable icon = ContextCompat.getDrawable(getActivity(), R.drawable.ic_launcher); + + return new Guidance( + title, + desc, + "", + icon + ); + } + + private void initCheckedItems() { + CheckedSource source = new KeyboardInfoAdapter(getActivity()); + for (CheckedItem item : source.getItems()) { + addCheckedAction(item.getTitle(), item::getChecked, item::onClick); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbSettingsFragment.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbSettingsFragment.java new file mode 100644 index 0000000..6362fa4 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbSettingsFragment.java @@ -0,0 +1,53 @@ +package com.liskovsoft.leankeyboard.fragments.settings; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.leanback.app.GuidedStepSupportFragment; +import androidx.leanback.widget.GuidanceStylist.Guidance; +import com.liskovsoft.leankeyboard.activity.settings.KbActivationActivity; +import com.liskovsoft.leankeykeyboard.R; + +public class KbSettingsFragment extends BaseSettingsFragment { + @Override + public void onAttach(Context context) { + super.onAttach(context); + + addNextAction(R.string.activate_keyboard, () -> { + Intent intent = new Intent(getActivity(), KbActivationActivity.class); + startActivity(intent); + }); + + addNextAction(R.string.change_layout, () -> startGuidedFragment(new KbLayoutFragment())); + + addNextAction(R.string.change_theme, () -> startGuidedFragment(new KbThemeFragment())); + + addNextAction(R.string.misc, () -> startGuidedFragment(new MiscFragment())); + + addNextAction(R.string.about_desc, () -> startGuidedFragment(new AboutFragment())); + } + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getActivity().getResources().getString(R.string.ime_name); + String desc = getActivity().getResources().getString(R.string.kb_settings_desc); + Drawable icon = ContextCompat.getDrawable(getActivity(), R.drawable.ic_launcher); + + return new Guidance( + title, + desc, + "", + icon + ); + } + + private void startGuidedFragment(GuidedStepSupportFragment fragment) { + if (getFragmentManager() != null) { + GuidedStepSupportFragment.add(getFragmentManager(), fragment); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbThemeFragment.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbThemeFragment.java new file mode 100644 index 0000000..7e13ab2 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/KbThemeFragment.java @@ -0,0 +1,51 @@ +package com.liskovsoft.leankeyboard.fragments.settings; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.leanback.widget.GuidanceStylist.Guidance; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; +import com.liskovsoft.leankeykeyboard.R; + +public class KbThemeFragment extends BaseSettingsFragment { + private Context mContext; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mContext = context; + + initRadioItems(); + } + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getActivity().getResources().getString(R.string.kb_theme); + String desc = getActivity().getResources().getString(R.string.kb_theme_desc); + Drawable icon = ContextCompat.getDrawable(getActivity(), R.drawable.ic_launcher); + + return new Guidance( + title, + desc, + "", + icon + ); + } + + private void initRadioItems() { + String[] themes = mContext.getResources().getStringArray(R.array.keyboard_themes); + + LeanKeyPreferences prefs = LeanKeyPreferences.instance(mContext); + String currentTheme = prefs.getCurrentTheme(); + + for (String theme : themes) { + String[] split = theme.split("\\|"); + String themeName = split[0]; + String themeId = split[1]; + addRadioAction(themeName, () -> currentTheme.equals(themeId), (checked) -> prefs.setCurrentTheme(themeId)); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/MiscFragment.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/MiscFragment.java new file mode 100644 index 0000000..979af4c --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/fragments/settings/MiscFragment.java @@ -0,0 +1,53 @@ +package com.liskovsoft.leankeyboard.fragments.settings; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.leanback.widget.GuidanceStylist.Guidance; +import com.liskovsoft.leankeyboard.activity.settings.KbSettingsActivity2; +import com.liskovsoft.leankeyboard.helpers.Helpers; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; +import com.liskovsoft.leankeykeyboard.R; + +public class MiscFragment extends BaseSettingsFragment { + private LeanKeyPreferences mPrefs; + private Context mContext; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + mContext = context; + mPrefs = LeanKeyPreferences.instance(getActivity()); + addCheckedAction(R.string.keep_on_screen, R.string.keep_on_screen_desc, mPrefs::getForceShowKeyboard, mPrefs::setForceShowKeyboard); + addCheckedAction(R.string.increase_kbd_size, R.string.increase_kbd_size_desc, mPrefs::getEnlargeKeyboard, mPrefs::setEnlargeKeyboard); + addCheckedAction(R.string.enable_suggestions, R.string.enable_suggestions_desc, mPrefs::getSuggestionsEnabled, mPrefs::setSuggestionsEnabled); + addCheckedAction(R.string.show_launcher_icon, R.string.show_launcher_icon_desc, this::getLauncherIconShown, this::setLauncherIconShown); + addCheckedAction(R.string.enable_cyclic_navigation, R.string.enable_cyclic_navigation_desc, mPrefs::isCyclicNavigationEnabled, mPrefs::setCyclicNavigationEnabled); + } + + @NonNull + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getActivity().getResources().getString(R.string.misc); + String desc = getActivity().getResources().getString(R.string.misc_desc); + Drawable icon = ContextCompat.getDrawable(getActivity(), R.drawable.ic_launcher); + + return new Guidance( + title, + desc, + "", + icon + ); + } + + private void setLauncherIconShown(boolean shown) { + Helpers.setLauncherIconShown(mContext, KbSettingsActivity2.class, shown); + } + + private boolean getLauncherIconShown() { + return Helpers.getLauncherIconShown(mContext, KbSettingsActivity2.class); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/AppInfoHelpers.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/AppInfoHelpers.java new file mode 100644 index 0000000..50bc724 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/AppInfoHelpers.java @@ -0,0 +1,156 @@ +package com.liskovsoft.leankeyboard.helpers; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ProviderInfo; +import android.content.res.Resources.NotFoundException; +import android.util.Log; +import androidx.core.content.pm.PackageInfoCompat; + +public class AppInfoHelpers { + private static final String TAG = AppInfoHelpers.class.getSimpleName(); + + public static String getAppVersion(Context context) { + return formatAppVersion(getAppVersionName(context), getActivityLabel(context)); + } + + public static String getAppVersionRobust(Context context, String launchActivityName) { + return formatAppVersion(getAppVersionName(context), getActivityLabelRobust(context, launchActivityName)); + } + + private static String formatAppVersion(String version, String label) { + return String.format("%s (%s)", version, label); + } + + public static String getActivityLabelRobust(Context context, String launchActivityName) { + return getActivityLabel(context, launchActivityName); + } + + public static int getAppVersionCode(Context context) { + PackageInfo packageInfo = createPackageInfo(context); + if (packageInfo != null) { + return (int) PackageInfoCompat.getLongVersionCode(packageInfo); + } + + return 0; + } + + public static String getAppVersionName(Context context) { + PackageInfo packageInfo = createPackageInfo(context); + if (packageInfo != null) { + return packageInfo.versionName; + } + + return null; + } + + private static PackageInfo createPackageInfo(Context context) { + try { + return context + .getPackageManager() + .getPackageInfo(context.getPackageName(), 0); + } catch (NameNotFoundException e) { + Log.d(TAG, e.getMessage()); + e.printStackTrace(); + } + + return null; + } + + public static String getActivityLabel(Context context) { + return getActivityLabel(context, (String) null); + } + + public static String getActivityLabel(Context context, String cls) { + if (cls != null) { + return getActivityLabel(context, new ComponentName(context, cls)); + } else if (context instanceof Activity) { + Activity activity = (Activity) context; + return getActivityLabel(context, activity.getComponentName()); + } + + return null; + } + + private static String getActivityLabel(Context context, ComponentName name) { + PackageManager pm = context.getPackageManager(); + + ActivityInfo info = null; + + try { + info = pm.getActivityInfo(name, 0); + return context.getResources().getString(info.labelRes); + } catch (NameNotFoundException | NotFoundException e) { + if (info != null) { + return Helpers.getSimpleClassName(info.name); // label not found, return simple class name + } + } + + return null; + } + + public static ActivityInfo getActivityInfo(Context ctx, ComponentName componentName) { + ActivityInfo ai = null; + try { + ai = ctx.getPackageManager().getActivityInfo(componentName, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + return ai; + } + + public static ProviderInfo getProviderInfo(Context ctx, ComponentName componentName) { + ProviderInfo ai = null; + try { + ai = ctx.getPackageManager().getProviderInfo(componentName, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + return ai; + } + + public static ActivityInfo[] getActivityList(Context context) { + PackageManager pm = context.getPackageManager(); + + ActivityInfo[] list = null; + + try { + PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES); + list = info.activities; + } catch (NameNotFoundException e) { + e.printStackTrace(); + } + + return list; + } + + public static boolean isActivityExists(Context context, String actName) { + ActivityInfo[] list = getActivityList(context); + + if (list != null) { + for (ActivityInfo activityInfo : list) { + if (activityInfo.name.contains(actName)) { + return true; + } + } + } + + return false; + } + + public static String getApplicationName(Context context) { + if (context == null) { + return null; + } + + ApplicationInfo applicationInfo = context.getApplicationInfo(); + int stringId = applicationInfo.labelRes; + return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : context.getString(stringId); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/Helpers.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/Helpers.java new file mode 100644 index 0000000..2b3af34 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/Helpers.java @@ -0,0 +1,251 @@ +package com.liskovsoft.leankeyboard.helpers; + +import android.app.Activity; +import android.app.ActivityManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import com.liskovsoft.leankeyboard.activity.settings.KbSettingsActivity; +import com.liskovsoft.leankeyboard.utils.LocaleUtility; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Helpers { + /** + * Simple wildcard matching routine. Implemented without regex. So you may expect huge performance boost. + * @param host + * @param mask + * @return + */ + public static boolean matchSubstr(String host, String mask) { + String[] sections = mask.split("\\*"); + String text = host; + for (String section : sections) { + int index = text.indexOf(section); + if (index == -1) { + return false; + } + text = text.substring(index + section.length()); + } + return true; + } + + public static boolean matchSubstrNoCase(String host, String mask) { + return matchSubstr(host.toLowerCase(), mask.toLowerCase()); + } + + public static InputStream getAsset(Context ctx, String fileName) { + InputStream is = null; + try { + is = ctx.getAssets().open(fileName); + } catch (IOException e) { + e.printStackTrace(); + } + return is; + } + + public static String encodeURI(byte[] data) { + try { + // make behaviour of java uri-encode the same as javascript's one + return URLEncoder.encode(new String(data, "UTF-8"), "UTF-8").replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static boolean floatEquals(float num1, float num2) { + float epsilon = 0.1f; + return Math.abs(num1 - num2) < epsilon; + } + + public static String getDeviceName() { + return String.format("%s (%s)", Build.MODEL, Build.PRODUCT); + } + + public static boolean deviceMatch(String[] devicesToProcess) { + String thisDeviceName = Helpers.getDeviceName(); + for (String deviceName : devicesToProcess) { + boolean match = matchSubstrNoCase(thisDeviceName, deviceName); + if (match) { + return true; + } + } + return false; + } + + public static String toString(Throwable ex) { + if (ex instanceof IllegalStateException && + ex.getCause() != null) { + ex = ex.getCause(); + } + return String.format("%s: %s", ex.getClass().getCanonicalName(), ex.getMessage()); + } + + public static String toString(InputStream inputStream) { + if (inputStream == null) { + return null; + } + + Scanner s = new Scanner(inputStream).useDelimiter("\\A"); + String result = s.hasNext() ? s.next() : ""; + return result; + } + + public static InputStream toStream(String content) { + return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + } + + public static void postOnUiThread(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + public static String unixToLocalDate(Context ctx, String timestamp) { + Locale current = LocaleUtility.getSystemLocale(ctx); + DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, current); + Date date; + if (timestamp == null) { + date = new Date(); + } else { + date = new Date((long) Integer.parseInt(timestamp) * 1000); + } + return dateFormat.format(date); + } + + public static String runMultiMatcher(String input, String... patterns) { + if (input == null) { + return null; + } + + Pattern regex; + Matcher matcher; + String result = null; + for (String pattern : patterns) { + regex = Pattern.compile(pattern); + matcher = regex.matcher(input); + + if (matcher.find()) { + result = matcher.group(matcher.groupCount()); // get last group + break; + } + } + + return result; + } + + public static boolean isCallable(Context ctx, Intent intent) { + List list = ctx.getPackageManager().queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } + + public static String getSimpleClassName(String name) { + if (name == null) { + return null; + } + + return name.substring(name.lastIndexOf('.') + 1); + } + + private static void killThisPackageProcess(Context context) { + Log.e("RestartServiceReceiver", "Attempting to kill org.liskovsoft.androidtv.rukeyboard process"); + ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE); + activityManager.killBackgroundProcesses(getPackageName(context)); + } + + private static void restartService(Context context) { + // START YOUR SERVICE HERE + Log.e("RestartServiceReceiver", "Restarting Service"); + //final Class serviceClass = classForName("com.google.leanback.ime.LeanbackImeService"); + //Intent serviceIntent = new Intent(context.getApplicationContext(), serviceClass); + Intent serviceIntent = new Intent(); + serviceIntent.setComponent(new ComponentName(getPackageName(context), "com.google.leanback.ime.LeanbackImeService")); + context.stopService(serviceIntent); + context.startService(serviceIntent); + } + + public static Class classForName(String clazz) { + Class serviceClass; + try { + serviceClass = Class.forName(clazz); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + return serviceClass; + } + + public static String getPackageName(Context ctx) { + return ctx.getPackageName(); + } + + public static void startActivity(Context context, Class activityClass) { + try { + Intent intent = new Intent(context, activityClass); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + MessageHelpers.showLongMessage(context, "Can't start: " + e.getMessage()); + } + } + + public static boolean startIntent(final Context context, final Intent intent) { + if (intent == null) { + return false; + } + + try { + context.startActivity(intent); + } catch (ActivityNotFoundException ex) { + return false; + } + + return true; + } + + public static boolean isGenymotion() { + String deviceName = getDeviceName(); + + return deviceName.contains("(vbox86p)"); + } + + public static void setLauncherIconShown(Context context, Class activityClass, boolean shown) { + PackageManager pm = context.getPackageManager(); + ComponentName component = new ComponentName(context, activityClass); + pm.setComponentEnabledSetting( + component, + shown ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ); + } + + public static boolean getLauncherIconShown(Context context, Class activityClass) { + PackageManager pm = context.getPackageManager(); + ComponentName component = new ComponentName(context, activityClass); + return pm.getComponentEnabledSetting(component) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/MessageHelpers.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/MessageHelpers.java new file mode 100644 index 0000000..119968f --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/MessageHelpers.java @@ -0,0 +1,90 @@ +package com.liskovsoft.leankeyboard.helpers; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +public class MessageHelpers { + private static long sExitMsgTimeMS = 0; + private static final int LONG_MSG_TIMEOUT = 5000; + + public static void showMessage(final Context ctx, final String TAG, final Throwable ex) { + showMessage(ctx, TAG, Helpers.toString(ex)); + } + + public static void showMessage(final Context ctx, final String TAG, final String msg) { + showMessage(ctx, String.format("%s: %s", TAG, msg)); + } + + public static void showMessageThrottled(final Context ctx, final String msg) { + // throttle msg calls + if (System.currentTimeMillis() - sExitMsgTimeMS < LONG_MSG_TIMEOUT) { + return; + } + sExitMsgTimeMS = System.currentTimeMillis(); + showMessage(ctx, msg); + } + + public static void showMessage(final Context ctx, final String msg) { + if (ctx == null) { + return; + } + + Runnable toast = () -> { + try { + Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show(); + } catch (Exception ex) { // NPE fix + ex.printStackTrace(); + } + }; + + if (Looper.myLooper() == Looper.getMainLooper()) { + toast.run(); + } else { + new Handler(Looper.getMainLooper()).post(toast); + } + } + + /** + * Shows long toast message.
+ * Uses resource id as message. + * @param ctx context + * @param resId resource id + */ + public static void showLongMessage(Context ctx, int resId) { + showLongMessage(ctx, ctx.getResources().getString(resId)); + } + + public static void showLongMessage(Context ctx, String msg) { + for (int i = 0; i < 3; i++) { + showMessage(ctx, msg); + } + } + + public static void showLongMessage(Context ctx, String TAG, String msg) { + for (int i = 0; i < 3; i++) { + showMessage(ctx, TAG, msg); + } + } + + /** + * Shows toast message.
+ * Uses resource id as message. + * @param ctx context + * @param resId resource id + */ + public static void showMessage(Context ctx, int resId) { + showMessage(ctx, ctx.getResources().getString(resId)); + } + + public static void showLongMessageEndPause(Context context, int resId) { + showLongMessage(context, resId); + + try { + Thread.sleep(5_000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/PermissionHelpers.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/PermissionHelpers.java new file mode 100644 index 0000000..59de2a6 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/helpers/PermissionHelpers.java @@ -0,0 +1,93 @@ +package com.liskovsoft.leankeyboard.helpers; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build.VERSION; +import androidx.core.app.ActivityCompat; + +@TargetApi(16) +public class PermissionHelpers { + // Storage Permissions + public static final int REQUEST_EXTERNAL_STORAGE = 112; + private static String[] PERMISSIONS_STORAGE = { + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + }; + + // Mic Permissions + public static final int REQUEST_MIC = 113; + private static String[] PERMISSIONS_MIC = { + Manifest.permission.RECORD_AUDIO + }; + + /** + * Checks if the app has permission to write to device storage
+ * If the app does not has permission then the user will be prompted to grant permissions
+ * Required for the {@link Context#getExternalCacheDir()}
+ * NOTE: runs async
+ * + * @param activity to apply permissions to + */ + public static void verifyStoragePermissions(Context activity) { + requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); + } + + public static void verifyMicPermissions(Context activity) { + requestPermissions(activity, PERMISSIONS_MIC, REQUEST_MIC); + } + + /** + * Only check. There is no prompt. + * @param activity to apply permissions to + * @return whether permission already granted + */ + public static boolean hasStoragePermissions(Context activity) { + // Check if we have write permission + return hasPermissions(activity, PERMISSIONS_STORAGE); + } + + public static boolean hasMicPermissions(Context activity) { + // Check if we have mic permission + return hasPermissions(activity, PERMISSIONS_MIC); + } + + // Utils + + /** + * Shows permissions dialog
+ * NOTE: runs async + */ + private static void requestPermissions(Context activity, String[] permissions, int requestId) { + if (!hasPermissions(activity, permissions) && !Helpers.isGenymotion()) { + if (activity instanceof Activity) { + // We don't have permission so prompt the user + ActivityCompat.requestPermissions( + (Activity) activity, + permissions, + requestId + ); + } + } + } + + /** + * Only check. There is no prompt. + * @param activity to apply permissions to + * @return whether permission already granted + */ + private static boolean hasPermissions(Context activity, String... permissions) { + if (VERSION.SDK_INT >= 23) { + for (String permission : permissions) { + int result = ActivityCompat.checkSelfPermission(activity, permission); + if (result != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + } + + return true; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/EventLogTags.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/EventLogTags.java new file mode 100644 index 0000000..465eb07 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/EventLogTags.java @@ -0,0 +1,16 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.util.EventLog; + +public class EventLogTags { + public static final int TIME_LEANBACK_IME_INPUT = 270900; + public static final int TOTAL_LEANBACK_IME_BACKSPACE = 270902; + + public static void writeTimeLeanbackImeInput(long time, long duration) { + EventLog.writeEvent(TIME_LEANBACK_IME_INPUT, new Object[]{time, duration}); + } + + public static void writeTotalLeanbackImeBackspace(int count) { + EventLog.writeEvent(TOTAL_LEANBACK_IME_BACKSPACE, count); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/KeyMapperImeService.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/KeyMapperImeService.java new file mode 100644 index 0000000..3ada986 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/KeyMapperImeService.java @@ -0,0 +1,107 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.inputmethodservice.InputMethodService; +import android.os.Build; +import android.os.Build.VERSION; +import android.view.KeyEvent; +import android.view.inputmethod.InputConnection; +import com.liskovsoft.leankeykeyboard.BuildConfig; + +public class KeyMapperImeService extends InputMethodService { + private static final String KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP = BuildConfig.APPLICATION_ID + ".inputmethod.ACTION_INPUT_DOWN_UP"; + private static final String KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN = BuildConfig.APPLICATION_ID + ".inputmethod.ACTION_INPUT_DOWN"; + private static final String KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP = BuildConfig.APPLICATION_ID + ".inputmethod.ACTION_INPUT_UP"; + private static final String KEY_MAPPER_INPUT_METHOD_ACTION_TEXT = BuildConfig.APPLICATION_ID + ".inputmethod.ACTION_INPUT_TEXT"; + private static final String KEY_MAPPER_INPUT_METHOD_EXTRA_TEXT = BuildConfig.APPLICATION_ID + ".inputmethod.EXTRA_TEXT"; + private static final String KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT = BuildConfig.APPLICATION_ID + ".inputmethod.EXTRA_KEY_EVENT"; + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } + + String action = intent.getAction(); + InputConnection currentInputConnection = getCurrentInputConnection(); + + if (currentInputConnection == null || action == null) { + return; + } + + KeyEvent downEvent; + KeyEvent upEvent; + + switch (action) { + case KEY_MAPPER_INPUT_METHOD_ACTION_TEXT: + String text = intent.getStringExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_TEXT); + if (text == null) { + return; + } + + currentInputConnection.commitText(text, 1); + break; + case KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP: + downEvent = intent.getParcelableExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT); + if (downEvent == null) { + return; + } + + currentInputConnection.sendKeyEvent(downEvent); + + upEvent = KeyEvent.changeAction(downEvent, KeyEvent.ACTION_UP); + currentInputConnection.sendKeyEvent(upEvent); + break; + case KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN: + downEvent = intent.getParcelableExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT); + if (downEvent == null) { + return; + } + + downEvent = KeyEvent.changeAction(downEvent, KeyEvent.ACTION_DOWN); + + currentInputConnection.sendKeyEvent(downEvent); + break; + case KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP: + upEvent = intent.getParcelableExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT); + if (upEvent == null) { + return; + } + + upEvent = KeyEvent.changeAction(upEvent, KeyEvent.ACTION_UP); + + currentInputConnection.sendKeyEvent(upEvent); + break; + } + } + }; + + @SuppressWarnings("UnspecifiedRegisterReceiverFlag") + @Override + public void onCreate() { + super.onCreate(); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN); + intentFilter.addAction(KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP); + intentFilter.addAction(KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP); + intentFilter.addAction(KEY_MAPPER_INPUT_METHOD_ACTION_TEXT); + + if (VERSION.SDK_INT < 33) { + registerReceiver(mBroadcastReceiver, intentFilter); + } else { + registerReceiver(mBroadcastReceiver, intentFilter, RECEIVER_EXPORTED); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + unregisterReceiver(mBroadcastReceiver); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackImeService.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackImeService.java new file mode 100644 index 0000000..cb39d57 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackImeService.java @@ -0,0 +1,403 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.content.Intent; +import android.inputmethodservice.InputMethodService; +import android.os.Build.VERSION; +import android.os.Handler; +import android.os.Message; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import androidx.core.text.BidiFormatter; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardController.InputListener; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; + +public class LeanbackImeService extends KeyMapperImeService { + private static final String TAG = LeanbackImeService.class.getSimpleName(); + private static final boolean DEBUG = false; + public static final String IME_CLOSE = "com.google.android.athome.action.IME_CLOSE"; + public static final String IME_OPEN = "com.google.android.athome.action.IME_OPEN"; + public static final int MAX_SUGGESTIONS = 10; + static final int MODE_FREE_MOVEMENT = 1; + static final int MODE_TRACKPAD_NAVIGATION = 0; + private static final int MSG_SUGGESTIONS_CLEAR = 123; + private static final int SUGGESTIONS_CLEAR_DELAY = 1000; + private boolean mEnterSpaceBeforeCommitting; + private View mInputView; + private LeanbackKeyboardController mKeyboardController; + private boolean mShouldClearSuggestions = true; + private LeanbackSuggestionsFactory mSuggestionsFactory; + public static final String COMMAND_RESTART = "restart"; + private boolean mForceShowKbd; + + @SuppressLint("HandlerLeak") + private final Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + if (msg.what == MSG_SUGGESTIONS_CLEAR && mShouldClearSuggestions) { + mSuggestionsFactory.clearSuggestions(); + mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); + mShouldClearSuggestions = false; + } + + } + }; + + private InputListener mInputListener = this::handleTextEntry; + + @SuppressLint("NewApi") + @SuppressWarnings("deprecation") + public LeanbackImeService() { + if (VERSION.SDK_INT < 21 && !enableHardwareAcceleration()) { + Log.w("LbImeService", "Could not enable hardware acceleration"); + } + } + + @Override + public void onCreate() { + //setupDensity(); + super.onCreate(); + + Log.d(TAG, "onCreate"); + + initSettings(); + } + + private void setupDensity() { + if (LeanKeyPreferences.instance(this).getEnlargeKeyboard()) { + DisplayMetrics metrics = LeanbackUtils.createMetricsFrom(this, 1.3f); + + if (metrics != null) { + getResources().getDisplayMetrics().setTo(metrics); + } + } + } + + private void initSettings() { + LeanKeyPreferences prefs = LeanKeyPreferences.instance(this); + mForceShowKbd = prefs.getForceShowKeyboard(); + + if (mKeyboardController != null) { + mKeyboardController.setSuggestionsEnabled(prefs.getSuggestionsEnabled()); + } + } + + private void clearSuggestionsDelayed() { + if (!mSuggestionsFactory.shouldSuggestionsAmend()) { + mHandler.removeMessages(MSG_SUGGESTIONS_CLEAR); + mShouldClearSuggestions = true; + mHandler.sendEmptyMessageDelayed(MSG_SUGGESTIONS_CLEAR, SUGGESTIONS_CLEAR_DELAY); + } + + } + + private void handleTextEntry(final int type, final int keyCode, final CharSequence text) { + final InputConnection connection = getCurrentInputConnection(); + if (connection != null) { + boolean updateSuggestions; + switch (type) { + case InputListener.ENTRY_TYPE_STRING: + clearSuggestionsDelayed(); + if (mEnterSpaceBeforeCommitting && mKeyboardController.enableAutoEnterSpace()) { + if (LeanbackUtils.isAlphabet(keyCode)) { + connection.commitText(" ", 1); + } + + mEnterSpaceBeforeCommitting = false; + } + + connection.commitText(text, 1); + updateSuggestions = true; + if (keyCode == LeanbackKeyboardView.ASCII_PERIOD) { + mEnterSpaceBeforeCommitting = true; + } + break; + case InputListener.ENTRY_TYPE_BACKSPACE: + clearSuggestionsDelayed(); + connection.deleteSurroundingText(1, 0); + mEnterSpaceBeforeCommitting = false; + updateSuggestions = true; + break; + case InputListener.ENTRY_TYPE_SUGGESTION: + case InputListener.ENTRY_TYPE_VOICE: + clearSuggestionsDelayed(); + if (!mSuggestionsFactory.shouldSuggestionsAmend()) { + connection.deleteSurroundingText(LeanbackUtils.getCharLengthBeforeCursor(connection), LeanbackUtils.getCharLengthAfterCursor(connection)); + } else { + int location = LeanbackUtils.getAmpersandLocation(connection); + connection.setSelection(location, location); + connection.deleteSurroundingText(0, LeanbackUtils.getCharLengthAfterCursor(connection)); + } + + connection.commitText(text, 1); + mEnterSpaceBeforeCommitting = true; + case InputListener.ENTRY_TYPE_ACTION: // User presses Go, Send, Search etc + boolean result = sendDefaultEditorAction(true); + + if (result) { + hideWindow(); // SmartYouTubeTV: hide kbd on search page fix + } else { + LeanbackUtils.sendEnterKey(connection); + } + + updateSuggestions = false; + break; + case InputListener.ENTRY_TYPE_LEFT: + case InputListener.ENTRY_TYPE_RIGHT: + BidiFormatter formatter = BidiFormatter.getInstance(); + + CharSequence textBeforeCursor = connection.getTextBeforeCursor(1000, 0); + int lenBefore = 0; + boolean isRtlBefore = false; + //int rtlLenBefore = 0; + if (textBeforeCursor != null) { + lenBefore = textBeforeCursor.length(); + isRtlBefore = formatter.isRtl(textBeforeCursor); + //rtlLenBefore = LeanbackUtils.getRtlLenBeforeCursor(textBeforeCursor); + } + + CharSequence textAfterCursor = connection.getTextAfterCursor(1000, 0); + int lenAfter = 0; + //int rtlLenAfter = 0; + boolean isRtlAfter = false; + if (textAfterCursor != null) { + lenAfter = textAfterCursor.length(); + isRtlAfter = formatter.isRtl(textAfterCursor); + //rtlLenAfter = LeanbackUtils.getRtlLenAfterCursor(textAfterCursor); + } + + int index = lenBefore; + if (type == InputListener.ENTRY_TYPE_LEFT) { + if (lenBefore > 0) { + if (!isRtlBefore) { + index = lenBefore - 1; + } else { + if (lenAfter == 0) { + index = 1; + } else if (lenAfter == 1) { + index = 0; + } else { + index = lenBefore + 1; + } + } + } + + //Log.d(TAG, String.format("direction key: before: lenBefore=%s, lenAfter=%s, rtlLenBefore=%s, rtlLenAfter=%s", lenBefore, lenAfter, rtlLenBefore, rtlLenAfter)); + Log.d(TAG, String.format("direction key: before: lenBefore=%s, lenAfter=%s, isRtlBefore=%s", lenBefore, lenAfter, isRtlBefore)); + } else { + if (lenAfter > 0) { + if (!isRtlAfter) { + index = lenBefore + 1; + } else { + if (lenBefore == 0) { + index = lenAfter - 1; + } else if (lenBefore == 1) { + index = lenAfter + 1; + } else { + index = lenBefore - 1; + } + } + } + + //Log.d(TAG, String.format("direction key: after: lenBefore=%s, lenAfter=%s, rtlLenBefore=%s, rtlLenAfter=%s", lenBefore, lenAfter, rtlLenBefore, rtlLenAfter)); + Log.d(TAG, String.format("direction key: after: lenBefore=%s, lenAfter=%s, isRtlAfter=%s", lenBefore, lenAfter, isRtlAfter)); + } + + Log.d(TAG, "direction key: index: " + index); + + connection.setSelection(index, index); + updateSuggestions = true; + break; + case InputListener.ENTRY_TYPE_DISMISS: + connection.performEditorAction(EditorInfo.IME_ACTION_NONE); + updateSuggestions = false; + break; + case InputListener.ENTRY_TYPE_VOICE_DISMISS: + connection.performEditorAction(EditorInfo.IME_ACTION_GO); + updateSuggestions = false; + break; + default: + updateSuggestions = true; + } + + if (mKeyboardController.areSuggestionsEnabled() && updateSuggestions) { + mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); + } + } + } + + @Override + public View onCreateInputView() { + mInputView = mKeyboardController.getView(); + mInputView.requestFocus(); + + return mInputView; + } + + @Override + public void onDisplayCompletions(CompletionInfo[] infos) { + if (mKeyboardController.areSuggestionsEnabled()) { + mShouldClearSuggestions = false; + mHandler.removeMessages(123); + mSuggestionsFactory.onDisplayCompletions(infos); + mKeyboardController.updateSuggestions(this.mSuggestionsFactory.getSuggestions()); + } + + } + + @Override + public boolean onEvaluateFullscreenMode() { + return false; // don't change it (true shows edit dialog above kbd) + } + + /** + * At this point, decision whether to show kbd taking place
+ * More info + * @return whether to show kbd + */ + @Override + public boolean onEvaluateInputViewShown() { + Log.d(TAG, "onEvaluateInputViewShown"); + return mForceShowKbd || super.onEvaluateInputViewShown(); + } + + // FireTV fix + @Override + public boolean onShowInputRequested(int flags, boolean configChange) { + Log.d(TAG, "onShowInputRequested"); + return mForceShowKbd || super.onShowInputRequested(flags, configChange); + } + + @Override + public void onFinishInputView(boolean finishingInput) { + super.onFinishInputView(finishingInput); + sendBroadcast(new Intent(IME_CLOSE)); + mSuggestionsFactory.clearSuggestions(); + + // NOTE: Trying to fix kbd without UI bug (telegram) + reInitKeyboard(); + } + + @SuppressLint("NewApi") + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + return isInputViewShown() && + (event.getSource() & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice.SOURCE_TOUCH_NAVIGATION && + mKeyboardController.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); + } + + @SuppressLint("WrongConstant") + public void hideIme() { + requestHideSelf(InputMethodService.BACK_DISPOSITION_DEFAULT); + } + + @Override + public void onInitializeInterface() { + mKeyboardController = new LeanbackKeyboardController(this, mInputListener); + mKeyboardController.setHideWhenPhysicalKeyboardUsed(!mForceShowKbd); + mEnterSpaceBeforeCommitting = false; + mSuggestionsFactory = new LeanbackSuggestionsFactory(this, MAX_SUGGESTIONS); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + //// DOESN'T WORK!!! + //// Hide keyboard on ESC key: https://github.com/yuliskov/SmartYouTubeTV/issues/142 + //event = mapEscToBack(event); + //keyCode = mapEscToBack(keyCode); + + // Hide keyboard on ESC key: https://github.com/yuliskov/SmartYouTubeTV/issues/142 + if (keyCode == KeyEvent.KEYCODE_ESCAPE) { + hideIme(); + return true; + } + + return isInputViewShown() && mKeyboardController.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + //// DOESN'T WORK!!! + //// Hide keyboard on ESC key: https://github.com/yuliskov/SmartYouTubeTV/issues/142 + //event = mapEscToBack(event); + //keyCode = mapEscToBack(keyCode); + + return isInputViewShown() && mKeyboardController.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); + } + + private KeyEvent mapEscToBack(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { + // pay attention, you must pass the same action + event = new KeyEvent(event.getAction(), KeyEvent.KEYCODE_BACK); + } + return event; + } + + private int mapEscToBack(int keyCode) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE) { + keyCode = KeyEvent.KEYCODE_BACK; + } + return keyCode; + } + + @Override + public int onStartCommand(final Intent intent, final int flags, final int startId) { + if (intent != null) { + Log.d(TAG, "onStartCommand: " + intent.toUri(0)); + + if (intent.getBooleanExtra(COMMAND_RESTART, false)) { + Log.d(TAG, "onStartCommand: trying to restart service"); + + reInitKeyboard(); + + return Service.START_REDELIVER_INTENT; + } + } + + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onStartInput(EditorInfo info, boolean restarting) { + super.onStartInput(info, restarting); + mEnterSpaceBeforeCommitting = false; + mSuggestionsFactory.onStartInput(info); + mKeyboardController.onStartInput(info); + } + + @Override + public void onStartInputView(EditorInfo info, boolean restarting) { + super.onStartInputView(info, restarting); + + mKeyboardController.onStartInputView(); + sendBroadcast(new Intent(IME_OPEN)); + if (mKeyboardController.areSuggestionsEnabled()) { + mSuggestionsFactory.createSuggestions(); + mKeyboardController.updateSuggestions(mSuggestionsFactory.getSuggestions()); + + // NOTE: FileManager+ rename item fix: https://t.me/LeanKeyboard/931 + // NOTE: Code below deletes text that has selection. + //InputConnection connection = getCurrentInputConnection(); + //if (connection != null) { + // String text = LeanbackUtils.getEditorText(connection); + // connection.deleteSurroundingText(LeanbackUtils.getCharLengthBeforeCursor(connection), LeanbackUtils.getCharLengthAfterCursor(connection)); + // connection.commitText(text, 1); + //} + } + } + + private void reInitKeyboard() { + initSettings(); + + if (mKeyboardController != null) { + mKeyboardController.initKeyboards(); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardContainer.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardContainer.java new file mode 100644 index 0000000..4d4c69a --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardContainer.java @@ -0,0 +1,1587 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.os.Build.VERSION; +import android.os.Bundle; +import android.speech.RecognitionListener; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; +import android.text.InputType; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.Transformation; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.RelativeLayout.LayoutParams; +import androidx.core.content.ContextCompat; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardManager.KeyboardData; +import com.liskovsoft.leankeyboard.addons.theme.ThemeManager; +import com.liskovsoft.leankeyboard.addons.voice.RecognizerIntentWrapper; +import com.liskovsoft.leankeyboard.helpers.PermissionHelpers; +import com.liskovsoft.leankeyboard.activity.PermissionsActivity; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardController.InputListener; +import com.liskovsoft.leankeyboard.ime.voice.RecognizerView; +import com.liskovsoft.leankeyboard.ime.voice.SpeechLevelSource; +import com.liskovsoft.leankeyboard.activity.settings.KbLayoutActivity; +import com.liskovsoft.leankeyboard.activity.settings.KbSettingsActivity; +import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardManager; +import com.liskovsoft.leankeyboard.helpers.Helpers; +import com.liskovsoft.leankeyboard.helpers.MessageHelpers; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class LeanbackKeyboardContainer { + private static final boolean DEBUG = false; + public static final double DIRECTION_STEP_MULTIPLIER = 1.25D; + private static final String IME_PRIVATE_OPTIONS_ESCAPE_NORTH = "EscapeNorth=1"; + private static final String IME_PRIVATE_OPTIONS_VOICE_DISMISS = "VoiceDismiss=1"; + private static final long MOVEMENT_ANIMATION_DURATION = 150L; + private static final int MSG_START_INPUT_VIEW = 0; + protected static final float PHYSICAL_HEIGHT_CM = 5.0F; + protected static final float PHYSICAL_WIDTH_CM = 12.0F; + private static final String TAG = "LbKbContainer"; + public static final double TOUCH_MOVE_MIN_DISTANCE = 0.1D; + public static final int TOUCH_STATE_CLICK = 3; + public static final int TOUCH_STATE_NO_TOUCH = 0; + public static final int TOUCH_STATE_TOUCH_MOVE = 2; + public static final int TOUCH_STATE_TOUCH_SNAP = 1; + private static final boolean VOICE_SUPPORTED = true; + public static final Interpolator sMovementInterpolator = new DecelerateInterpolator(1.5F); + public static final int DIRECTION_UP = 8; + public static final int DIRECTION_DOWN = 2; + public static final int DIRECTION_LEFT = 1; + public static final int DIRECTION_RIGHT = 4; + private Keyboard mAbcKeyboard; + private Button mActionButtonView; + private final float mAlphaIn; + private final float mAlphaOut; + private boolean mAutoEnterSpaceEnabled; + private boolean mCapCharacters; + private boolean mCapSentences; + private boolean mCapWords; + private final int mClickAnimDur; + private LeanbackImeService mContext; + private KeyFocus mCurrKeyInfo = new KeyFocus(); + private DismissListener mDismissListener; + private KeyFocus mDownKeyInfo = new KeyFocus(); + private CharSequence mEnterKeyText; + private int mEnterKeyTextResId; + private boolean mEscapeNorthEnabled; + private Keyboard mInitialMainKeyboard; + private KeyboardManager mKeyboardManager; + private View mKeyboardsContainer; + private LeanbackKeyboardView mMainKeyboardView; + private int mMiniKbKeyIndex; + private Keyboard mNumKeyboard; + private float mOverestimate; + private PointF mPhysicalSelectPos = new PointF(2.0F, 0.5F); + private PointF mPhysicalTouchPos = new PointF(2.0F, 0.5F); + private LeanbackKeyboardView mPrevView; + private Intent mRecognizerIntent; + private Rect mRect = new Rect(); + private RelativeLayout mRootView; + private View mSelector; + private ImageView mKeySelector; + private Drawable mKeySelectorSquare; + private Drawable mKeySelectorStretched; + private ThemeManager mThemeManager; + private ScaleAnimation mSelectorAnimation; + private ValueAnimator mSelectorAnimator; + private SpeechLevelSource mSpeechLevelSource; + private SpeechRecognizer mSpeechRecognizer; + private RecognizerIntentWrapper mRecognizerIntentWrapper; + private LinearLayout mSuggestions; + private View mSuggestionsBg; + private HorizontalScrollView mSuggestionsContainer; + private boolean mSuggestionsEnabled; + private boolean mForceDisableSuggestions; + private Keyboard mSymKeyboard; + private KeyFocus mTempKeyInfo = new KeyFocus(); + private PointF mTempPoint = new PointF(); + private boolean mTouchDown = false; + private int mTouchState = TOUCH_STATE_NO_TOUCH; + private final int mVoiceAnimDur; + private final VoiceIntroAnimator mVoiceAnimator; + private RecognizerView mVoiceButtonView; + private boolean mVoiceEnabled; + private boolean mVoiceKeyDismissesEnabled; + private VoiceListener mVoiceListener; + private boolean mVoiceOn; + private Float mX; + private Float mY; + private String mLabel; + + private AnimatorListener mVoiceEnterListener = new AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationStart(Animator animation) { + mSelector.setVisibility(View.INVISIBLE); + startRecognition(mContext); + } + }; + + private AnimatorListener mVoiceExitListener = new AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mSelector.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationStart(Animator animation) { + mVoiceButtonView.showNotListening(); + mSpeechRecognizer.cancel(); + mSpeechRecognizer.setRecognitionListener(null); + mVoiceOn = false; + } + }; + + public LeanbackKeyboardContainer(Context context) { + mContext = (LeanbackImeService) context; + final Resources res = mContext.getResources(); + mVoiceAnimDur = res.getInteger(R.integer.voice_anim_duration); + mAlphaIn = res.getFraction(R.fraction.alpha_in, 1, 1); + mAlphaOut = res.getFraction(R.fraction.alpha_out, 1, 1); + mVoiceAnimator = new LeanbackKeyboardContainer.VoiceIntroAnimator(mVoiceEnterListener, mVoiceExitListener); + mRootView = (RelativeLayout) mContext.getLayoutInflater().inflate(R.layout.root_leanback, null); + mKeyboardsContainer = mRootView.findViewById(R.id.keyboard); + mSuggestionsBg = mRootView.findViewById(R.id.candidate_background); + mSuggestionsContainer = (HorizontalScrollView) mRootView.findViewById(R.id.suggestions_container); + mSuggestions = (LinearLayout) mSuggestionsContainer.findViewById(R.id.suggestions); + mMainKeyboardView = (LeanbackKeyboardView) mRootView.findViewById(R.id.main_keyboard); + mVoiceButtonView = (RecognizerView) mRootView.findViewById(R.id.voice); + mActionButtonView = (Button) mRootView.findViewById(R.id.enter); + mSelector = mRootView.findViewById(R.id.selector); + mKeySelector = mRootView.findViewById(R.id.key_selector); + mKeySelectorSquare = ContextCompat.getDrawable(mContext, R.drawable.key_selector_square); + mKeySelectorStretched = ContextCompat.getDrawable(mContext, R.drawable.key_selector); + mThemeManager = new ThemeManager(mContext, mRootView); + mSelectorAnimation = new ScaleAnimation((FrameLayout) mSelector); + mOverestimate = mContext.getResources().getFraction(R.fraction.focused_scale, 1, 1); + final float scale = context.getResources().getFraction(R.fraction.clicked_scale, 1, 1); + mClickAnimDur = context.getResources().getInteger(R.integer.clicked_anim_duration); + mSelectorAnimator = ValueAnimator.ofFloat(1.0F, scale); + mSelectorAnimator.setDuration(mClickAnimDur); + mSelectorAnimator.addUpdateListener(animation -> { + final float value = (Float) animation.getAnimatedValue(); + mSelector.setScaleX(value); + mSelector.setScaleY(value); + }); + mSpeechLevelSource = new SpeechLevelSource(); + mVoiceButtonView.setSpeechLevelSource(mSpeechLevelSource); + mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(mContext); + mRecognizerIntentWrapper = new RecognizerIntentWrapper(mContext); + mVoiceButtonView.setCallback(new RecognizerView.Callback() { + @Override + public void onCancelRecordingClicked() { + cancelVoiceRecording(); + } + + @Override + public void onStartRecordingClicked() { + startVoiceRecording(); + } + + @Override + public void onStopRecordingClicked() { + cancelVoiceRecording(); + } + }); + mKeyboardManager = new KeyboardManager(mContext); + initKeyboards(); + } + + private void configureFocus(KeyFocus focus, Rect rect, int index, int type) { + focus.type = type; + focus.index = index; + focus.rect.set(rect); + } + + /** + * NOTE: Initialize {@link KeyFocus} with values + * @param focus {@link KeyFocus} to configure + * @param rect {@link Rect} + * @param index key index + * @param key {@link Key} + * @param type {@link KeyFocus#type} constant + */ + private void configureFocus(KeyFocus focus, Rect rect, int index, Key key, int type) { + focus.type = type; + if (key != null) { + if (key.codes != null) { + focus.code = key.codes[0]; + } else { + focus.code = 0; + } + + focus.index = index; + focus.label = key.label; + focus.rect.left = key.x + rect.left; + focus.rect.top = key.y + rect.top; + focus.rect.right = focus.rect.left + key.width; + focus.rect.bottom = focus.rect.top + key.height; + } + } + + private void escapeNorth() { + mDismissListener.onDismiss(false); + } + + private PointF getAlignmentPosition(final float posXCm, final float posYCm, final PointF result) { + final float width = (float) (mRootView.getWidth() - mRootView.getPaddingRight() - mRootView.getPaddingLeft()); + final float height = (float) (mRootView.getHeight() - mRootView.getPaddingTop() - mRootView.getPaddingBottom()); + final float size = mContext.getResources().getDimension(R.dimen.selector_size); + result.x = posXCm / PHYSICAL_WIDTH_CM * (width - size) + (float) mRootView.getPaddingLeft(); + result.y = posYCm / PHYSICAL_HEIGHT_CM * (height - size) + (float) mRootView.getPaddingTop(); + return result; + } + + private void getPhysicalPosition(final float x, final float y, final PointF position) { + float width = (float) (mSelector.getWidth() / 2); + float height = (float) (mSelector.getHeight() / 2); + float posXCm = (float) (mRootView.getWidth() - mRootView.getPaddingRight() - mRootView.getPaddingLeft()); + float posYCm = (float) (mRootView.getHeight() - mRootView.getPaddingTop() - mRootView.getPaddingBottom()); + float size = mContext.getResources().getDimension(R.dimen.selector_size); + position.x = (x - width - (float) mRootView.getPaddingLeft()) * PHYSICAL_WIDTH_CM / (posXCm - size); + position.y = (y - height - (float) mRootView.getPaddingTop()) * PHYSICAL_HEIGHT_CM / (posYCm - size); + } + + private PointF getTouchSnapPosition() { + PointF position = new PointF(); + getPhysicalPosition((float) mCurrKeyInfo.rect.centerX(), (float) mCurrKeyInfo.rect.centerY(), position); + return position; + } + + public void initKeyboards() { + updateAddonKeyboard(); + } + + private boolean isMatch(Locale var1, Locale[] var2) { + int var4 = var2.length; + + for (int var3 = 0; var3 < var4; ++var3) { + Locale var5 = var2[var3]; + if ((TextUtils.isEmpty(var5.getLanguage()) || TextUtils.equals(var1.getLanguage(), var5.getLanguage())) && (TextUtils.isEmpty + (var5.getCountry()) || TextUtils.equals(var1.getCountry(), var5.getCountry()))) { + return true; + } + } + + return false; + } + + /** + * NOTE: Move focus to specified key + * @param index key index + * @param type {@link KeyFocus#type} constant + */ + private void moveFocusToIndex(int index, int type) { + Key key = mMainKeyboardView.getKey(index); + configureFocus(mTempKeyInfo, mRect, index, key, type); + setTouchState(TOUCH_STATE_NO_TOUCH); + setKbFocus(mTempKeyInfo, true, true); + } + + private void offsetRect(Rect rect, View view) { + rect.left = 0; + rect.top = 0; + rect.right = view.getWidth(); + rect.bottom = view.getHeight(); + mRootView.offsetDescendantRectToMyCoords(view, rect); + } + + private void onToggleCapsLock() { + onShiftDoubleClick(isCapsLockOn()); + } + + /** + * NOTE: Init currently displayed keyboard
+ * All keyboard settings applied here
+ * This method is called constantly on new field + * @param res resources (not used) + * @param info current ime attributes + */ + private void setImeOptions(Resources res, EditorInfo info) { + // do not erase last keyboard + if (mInitialMainKeyboard == null) { + mInitialMainKeyboard = mAbcKeyboard; + } + + mSuggestionsEnabled = true; + mAutoEnterSpaceEnabled = false; + mVoiceEnabled = true; + mEscapeNorthEnabled = false; + mVoiceKeyDismissesEnabled = false; + + switch (LeanbackUtils.getInputTypeClass(info)) { + case InputType.TYPE_CLASS_TEXT: + switch (LeanbackUtils.getInputTypeVariation(info)) { + case InputType.TYPE_DATETIME_VARIATION_DATE: + case InputType.TYPE_DATETIME_VARIATION_TIME: + case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + mSuggestionsEnabled = true; + mAutoEnterSpaceEnabled = false; + mVoiceEnabled = true; + mInitialMainKeyboard = mAbcKeyboard; + break; + case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + mSuggestionsEnabled = true; // use suggestion widget as input indicator + mVoiceEnabled = false; + mInitialMainKeyboard = mAbcKeyboard; + } + break; + case InputType.TYPE_CLASS_NUMBER: + case InputType.TYPE_CLASS_PHONE: + case InputType.TYPE_CLASS_DATETIME: + mSuggestionsEnabled = true; + mVoiceEnabled = false; + mInitialMainKeyboard = mAbcKeyboard; + } + + if (mSuggestionsEnabled) { + if ((info.inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) { + mSuggestionsEnabled = false; + } + } + + // NOTE: bug fix: any field: first char in upper case + //if (!mAutoEnterSpaceEnabled) { + // if ((info.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) { + // mCapSentences = true; + // } + //} + + if (mAutoEnterSpaceEnabled && !mSuggestionsEnabled) { + mAutoEnterSpaceEnabled = false; + } + + // NOTE: bug fix: any field: first char in upper case + //if ((info.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0 || + // LeanbackUtils.getInputTypeVariation(info) == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) { + // mCapWords = true; + //} + + // NOTE: bug fix: password field: all chars in upper case + //if ((info.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) { + // mCapCharacters = true; + //} + + if (mForceDisableSuggestions) { + mSuggestionsEnabled = false; + } + + if (info.privateImeOptions != null) { + if (info.privateImeOptions.contains(IME_PRIVATE_OPTIONS_ESCAPE_NORTH)) { + mEscapeNorthEnabled = true; + } + + if (info.privateImeOptions.contains(IME_PRIVATE_OPTIONS_VOICE_DISMISS)) { + mVoiceKeyDismissesEnabled = true; + } + } + + mEnterKeyText = info.actionLabel; + if (TextUtils.isEmpty(mEnterKeyText)) { + switch (LeanbackUtils.getImeAction(info)) { + case EditorInfo.IME_ACTION_GO: + mEnterKeyTextResId = R.string.label_go_key; + return; + case EditorInfo.IME_ACTION_SEARCH: + mEnterKeyTextResId = R.string.label_search_key; + return; + case EditorInfo.IME_ACTION_SEND: + mEnterKeyTextResId = R.string.label_send_key; + return; + case EditorInfo.IME_ACTION_NEXT: + mEnterKeyTextResId = R.string.label_next_key; + return; + default: + mEnterKeyTextResId = R.string.label_done_key; + } + } + + } + + /** + * Move focus to specified key + * @param focus key that will be focused + * @param forceFocusChange force focus + * @param animate animate transition + */ + private void setKbFocus(final KeyFocus focus, final boolean forceFocusChange, final boolean animate) { + boolean clicked = true; + if (!focus.equals(mCurrKeyInfo) || forceFocusChange) { + LeanbackKeyboardView prevView = mPrevView; + mPrevView = null; + boolean overestimateWidth = false; + boolean overestimateHeight = false; + switch (focus.type) { + case KeyFocus.TYPE_MAIN: + boolean showScale = false; + overestimateHeight = true; + if (focus.code != LeanbackKeyboardView.ASCII_SPACE) { + overestimateWidth = true; + showScale = true; + } + + LeanbackKeyboardView mainView = mMainKeyboardView; + int index = focus.index; + + boolean isClicked = false; + if (mTouchState == TOUCH_STATE_CLICK) { + isClicked = true; + } + + mainView.setFocus(index, isClicked, showScale); + mPrevView = mMainKeyboardView; + break; + case KeyFocus.TYPE_VOICE: + mVoiceButtonView.setMicFocused(true); + dismissMiniKeyboard(); + break; + case KeyFocus.TYPE_ACTION: + LeanbackUtils.sendAccessibilityEvent(mActionButtonView, true); + dismissMiniKeyboard(); + break; + case KeyFocus.TYPE_SUGGESTION: + dismissMiniKeyboard(); + } + + if (prevView != null && prevView != mPrevView) { + if (mTouchState != TOUCH_STATE_CLICK) { + clicked = false; + } + + prevView.setFocus(-1, clicked); + } + + setSelectorToFocus(focus.rect, overestimateWidth, overestimateHeight, animate); + mCurrKeyInfo.set(focus); + } + } + + /** + * Set keyboard shift sate + * @param state one of the + * {@link LeanbackKeyboardView#SHIFT_ON SHIFT_ON}, + * {@link LeanbackKeyboardView#SHIFT_OFF SHIFT_OFF}, + * {@link LeanbackKeyboardView#SHIFT_LOCKED SHIFT_LOCKED} + * constants + */ + private void setShiftState(int state) { + mMainKeyboardView.setShiftState(state); + } + + private void setTouchStateInternal(int state) { + mTouchState = state; + } + + /** + * NOTE: Speech recognizer routine + * @param context context + */ + private void startRecognition(Context context) { + // MANAGE_EXTERNAL_STORAGE does not work on Android 14 + if ((PermissionHelpers.hasStoragePermissions(context) || VERSION.SDK_INT >= 34) && + PermissionHelpers.hasMicPermissions(context)) { + if (SpeechRecognizer.isRecognitionAvailable(context)) { + mRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + mRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + mRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); + mSpeechRecognizer.setRecognitionListener(new MyVoiceRecognitionListener()); + + mSpeechRecognizer.startListening(mRecognizerIntent); + } else { + mRecognizerIntentWrapper.setListener(searchText -> mVoiceListener.onVoiceResult(searchText)); + mRecognizerIntentWrapper.startListening(); + + //String noRecognition = "Seems that the voice recognition is not enabled on your device"; + // + //MessageHelpers.showLongMessage(context, noRecognition); + // + //Log.e(TAG, noRecognition); + } + } else { + Helpers.startActivity(context, PermissionsActivity.class); + } + } + + public void alignSelector(final float x, final float y, final boolean playAnimation) { + final float translatedX = x - (float) (mSelector.getWidth() / 2); + final float translatedY = y - (float) (mSelector.getHeight() / 2); + if (!playAnimation) { + mSelector.setX(translatedX); + mSelector.setY(translatedY); + } else { + mSelector.animate() + .x(translatedX) + .y(translatedY) + .setInterpolator(sMovementInterpolator) + .setDuration(MOVEMENT_ANIMATION_DURATION) + .start(); + } + } + + public boolean areSuggestionsEnabled() { + return mSuggestionsEnabled; + } + + public void setSuggestionsEnabled(boolean enabled) { + mSuggestionsEnabled = enabled; + mForceDisableSuggestions = !enabled; + } + + public void cancelVoiceRecording() { + mVoiceAnimator.startExitAnimation(); + } + + public void clearSuggestions() { + mSuggestions.removeAllViews(); + if (getCurrFocus().type == KeyFocus.TYPE_SUGGESTION) { + resetFocusCursor(); + } + } + + public boolean dismissMiniKeyboard() { + return mMainKeyboardView.dismissMiniKeyboard(); + } + + public boolean enableAutoEnterSpace() { + return mAutoEnterSpaceEnabled; + } + + /** + * Initialize {@link KeyFocus focus} variable based on supplied coordinates + * @param x x coordinates + * @param y y coordinates + * @param focus result focus + * @return whether focus is found or not + */ + public boolean getBestFocus(final Float x, final Float y, final KeyFocus focus) { + offsetRect(mRect, mActionButtonView); + int actionLeft = mRect.left; + offsetRect(mRect, mMainKeyboardView); + int keyboardTop = mRect.top; + Float newX = x; + if (x == null) { + newX = mX; + } + + Float newY = y; + if (y == null) { + newY = mY; + } + + int count = mSuggestions.getChildCount(); + if (newY < (float) keyboardTop && count > 0 && mSuggestionsEnabled) { + for (actionLeft = 0; actionLeft < count; ++actionLeft) { + View view = mSuggestions.getChildAt(actionLeft); + offsetRect(mRect, view); + if (newX < (float) mRect.right || actionLeft + 1 == count) { + view.requestFocus(); + LeanbackUtils.sendAccessibilityEvent(view.findViewById(R.id.text), true); + configureFocus(focus, mRect, actionLeft, KeyFocus.TYPE_SUGGESTION); + break; + } + } + + return true; + } else if (newY < (float) keyboardTop && mEscapeNorthEnabled) { + escapeNorth(); + return false; + } else if (newX > (float) actionLeft) { + offsetRect(mRect, mActionButtonView); + configureFocus(focus, mRect, 0, KeyFocus.TYPE_ACTION); + return true; + } else { + mX = newX; + mY = newY; + offsetRect(mRect, mMainKeyboardView); + final float left = (float) mRect.left; + final float top = (float) mRect.top; + int keyIdx = mMainKeyboardView.getNearestIndex(newX - left, newY - top); + Key key = mMainKeyboardView.getKey(keyIdx); + configureFocus(focus, mRect, keyIdx, key, 0); + return true; + } + } + + public LeanbackKeyboardContainer.KeyFocus getCurrFocus() { + return mCurrKeyInfo; + } + + public int getCurrKeyCode() { + int keyCode = 0; + Key key = getKey(mCurrKeyInfo.type, mCurrKeyInfo.index); + if (key != null) { + keyCode = key.codes[0]; + } + + return keyCode; + } + + public Button getGoButton() { + return mActionButtonView; + } + + public Key getKey(int type, int index) { + return type == KeyFocus.TYPE_MAIN ? this.mMainKeyboardView.getKey(index) : null; + } + + public void updateCyclicFocus(int dir, KeyFocus oldFocus, KeyFocus newFocus) { + if (oldFocus.equals(newFocus) || LeanbackUtils.isSubmitButton(newFocus)) { + if (LeanKeyPreferences.instance(mContext).isCyclicNavigationEnabled()) { + if (dir == DIRECTION_RIGHT || dir == DIRECTION_LEFT) { + Rect actionRect = new Rect(); + offsetRect(actionRect, mActionButtonView); + boolean onSameRow = Math.abs(oldFocus.rect.top - actionRect.top) < 20; + + if (onSameRow && !LeanbackUtils.isSubmitButton(oldFocus)) { + // move focus to submit button + offsetRect(mRect, mActionButtonView); + configureFocus(newFocus, mRect, 0, KeyFocus.TYPE_ACTION); + } else { + offsetRect(mRect, mMainKeyboardView); + float x = dir == DIRECTION_RIGHT ? 0 : mRect.right; // 0 - rightmost position, right - leftmost + int keyIdx = mMainKeyboardView.getNearestIndex(x, oldFocus.rect.top - mRect.top); + Key key = mMainKeyboardView.getKey(keyIdx); + configureFocus(newFocus, mRect, keyIdx, key, 0); + } + } else if (dir == DIRECTION_DOWN || dir == DIRECTION_UP) { + if (!LeanbackUtils.isSubmitButton(oldFocus)) { + offsetRect(mRect, mMainKeyboardView); + float y = dir == DIRECTION_DOWN ? 0 : mRect.bottom; // 0 - topmost position, bottom - downmost + int delta = (oldFocus.rect.right - oldFocus.rect.left) / 2; // fix space position + int keyIdx = mMainKeyboardView.getNearestIndex(oldFocus.rect.left + delta - mRect.left, y); + Key key = mMainKeyboardView.getKey(keyIdx); + configureFocus(newFocus, mRect, keyIdx, key, 0); + } + } + } else if (dir == DIRECTION_UP) { + // Hide the keyboard when moving focus out of the keyboard + mContext.hideIme(); + } + + String direction = "UNKNOWN"; + + switch (dir) { + case LeanbackKeyboardContainer.DIRECTION_DOWN: + direction = "DOWN"; + break; + case LeanbackKeyboardContainer.DIRECTION_LEFT: + direction = "LEFT"; + break; + case LeanbackKeyboardContainer.DIRECTION_RIGHT: + direction = "RIGHT"; + break; + case LeanbackKeyboardContainer.DIRECTION_UP: + direction = "UP"; + break; + } + + Log.d(TAG, "Same key focus found! Direction: " + direction + " Key Label: " + oldFocus.label); + } + } + + public boolean getNextFocusInDirection(int direction, KeyFocus startFocus, KeyFocus nextFocus) { + switch (startFocus.type) { + case KeyFocus.TYPE_MAIN: + Key key = getKey(startFocus.type, startFocus.index); + float centerDelta = (float) startFocus.rect.height() / 2.0F; + float centerX = (float) startFocus.rect.centerX(); + float centerY = (float) startFocus.rect.centerY(); + if (startFocus.code == LeanbackKeyboardView.ASCII_SPACE) { + centerX = mX; + } + + if ((direction & DIRECTION_LEFT) != 0) { + if ((key.edgeFlags & Keyboard.EDGE_LEFT) == 0) { + centerX = (float) startFocus.rect.left - centerDelta; + } + } else if ((direction & DIRECTION_RIGHT) != 0) { + if ((key.edgeFlags & Keyboard.EDGE_RIGHT) != 0) { + offsetRect(mRect, mActionButtonView); + centerX = (float) mRect.centerX(); + } else { + centerX = (float) startFocus.rect.right + centerDelta; + } + } + + if ((direction & DIRECTION_UP) != 0) { + centerDelta = (float) ((double) centerY - (double) startFocus.rect.height() * DIRECTION_STEP_MULTIPLIER); + } else { + centerDelta = centerY; + if ((direction & DIRECTION_DOWN) != 0) { + centerDelta = (float) ((double) centerY + (double) startFocus.rect.height() * DIRECTION_STEP_MULTIPLIER); + } + } + + getPhysicalPosition(centerX, centerDelta, mTempPoint); + return getBestFocus(centerX, centerDelta, nextFocus); + case KeyFocus.TYPE_VOICE: + default: + break; + case KeyFocus.TYPE_ACTION: + offsetRect(mRect, mMainKeyboardView); + if ((direction & DIRECTION_LEFT) != 0) { + return getBestFocus((float) mRect.right, null, nextFocus); + } + + if ((direction & DIRECTION_UP) != 0) { + offsetRect(mRect, mSuggestions); + return getBestFocus((float) startFocus.rect.centerX(), (float) mRect.centerY(), nextFocus); + } + break; + case KeyFocus.TYPE_SUGGESTION: + if ((direction & DIRECTION_DOWN) != 0) { + offsetRect(mRect, mMainKeyboardView); + return getBestFocus((float) startFocus.rect.centerX(), (float) mRect.top, nextFocus); + } + + if ((direction & DIRECTION_UP) != 0) { + if (mEscapeNorthEnabled) { + escapeNorth(); + return true; + } + } else { + boolean left = (direction & DIRECTION_LEFT) != 0; + + boolean right = (direction & DIRECTION_RIGHT) != 0; + + if (left || right) { + offsetRect(mRect, mRootView); + MarginLayoutParams params = (MarginLayoutParams) mSuggestionsContainer.getLayoutParams(); + int leftCalc = mRect.left + params.leftMargin; + int rightCalc = mRect.right - params.rightMargin; + int focusIdx = startFocus.index; + byte delta; + if (left) { + delta = -1; + } else { + delta = 1; + } + + int suggestIdx = focusIdx + delta; + View suggestion = mSuggestions.getChildAt(suggestIdx); + if (suggestion != null) { + offsetRect(mRect, suggestion); + if (mRect.left < leftCalc && mRect.right > rightCalc) { + mRect.left = leftCalc; + mRect.right = rightCalc; + } else if (mRect.left < leftCalc) { + mRect.right = mRect.width() + leftCalc; + mRect.left = leftCalc; + } else if (mRect.right > rightCalc) { + mRect.left = rightCalc - mRect.width(); + mRect.right = rightCalc; + } + + suggestion.requestFocus(); + LeanbackUtils.sendAccessibilityEvent(suggestion.findViewById(R.id.text), true); + configureFocus(nextFocus, mRect, suggestIdx, KeyFocus.TYPE_SUGGESTION); + return true; + } + } + } + } + + return true; + } + + public CharSequence getSuggestionText(int idx) { + CharSequence result = null; + if (idx >= 0) { + if (idx < mSuggestions.getChildCount()) { + Button btn = mSuggestions.getChildAt(idx).findViewById(R.id.text); + if (btn != null) { + result = btn.getText(); + } + } + } + + return result; + } + + public int getTouchState() { + return mTouchState; + } + + public RelativeLayout getView() { + return mRootView; + } + + public boolean isCapsLockOn() { + return mMainKeyboardView.getShiftState() == LeanbackKeyboardView.SHIFT_LOCKED; + } + + public boolean isCurrKeyShifted() { + return mMainKeyboardView.isShifted(); + } + + public boolean isMiniKeyboardOnScreen() { + return mMainKeyboardView.isMiniKeyboardOnScreen(); + } + + public boolean isVoiceEnabled() { + return mVoiceEnabled; + } + + public boolean isVoiceVisible() { + return mVoiceButtonView.getVisibility() == View.VISIBLE; + } + + public void onInitInputView() { + resetFocusCursor(); + mSelector.setVisibility(View.VISIBLE); + } + + public boolean onKeyLongPress() { + int keyCode = mCurrKeyInfo.code; + if (keyCode == LeanbackKeyboardView.KEYCODE_SHIFT) { + onToggleCapsLock(); + setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_NO_TOUCH); + return true; + } else if (keyCode == LeanbackKeyboardView.ASCII_SPACE) { + LeanbackUtils.showKeyboardPicker(mContext); + // Keyboard may stuck on screen. Fixing it... + mContext.stopSelf(); + // Revert button touch states to normal + setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_NO_TOUCH); + return true; + } else if (keyCode == LeanbackKeyboardView.KEYCODE_LANG_TOGGLE) { + Helpers.startActivity(mContext, KbSettingsActivity.class); + mContext.hideIme(); + return true; + } else { + if (mCurrKeyInfo.type == KeyFocus.TYPE_MAIN) { + mMainKeyboardView.onKeyLongPress(); + if (mMainKeyboardView.isMiniKeyboardOnScreen()) { + mMiniKbKeyIndex = mCurrKeyInfo.index; + moveFocusToIndex(mMainKeyboardView.getBaseMiniKbIndex(), KeyFocus.TYPE_MAIN); + return true; + } + } + + return false; + } + } + + public void onModeChangeClick() { + dismissMiniKeyboard(); + if (mMainKeyboardView.getKeyboard().equals(mSymKeyboard)) { + mMainKeyboardView.setKeyboard(mInitialMainKeyboard); + } else { + mMainKeyboardView.setKeyboard(mSymKeyboard); + } + } + + public void onPeriodEntry() { + if (mMainKeyboardView.isShifted()) { + if (!isCapsLockOn() && !mCapCharacters && !mCapWords && !mCapSentences) { + setShiftState(LeanbackKeyboardView.SHIFT_OFF); + } + } else if (isCapsLockOn() || mCapCharacters || mCapWords || mCapSentences) { + setShiftState(LeanbackKeyboardView.SHIFT_ON); + } + } + + public void onShiftClick() { + byte state; + if (mMainKeyboardView.isShifted()) { + state = LeanbackKeyboardView.SHIFT_OFF; + } else { + state = LeanbackKeyboardView.SHIFT_ON; + } + + setShiftState(state); + } + + public void onShiftDoubleClick(boolean wasCapsLockOn) { + byte state; + if (wasCapsLockOn) { + state = LeanbackKeyboardView.SHIFT_OFF; + } else { + state = LeanbackKeyboardView.SHIFT_LOCKED; + } + + setShiftState(state); + } + + public void onSpaceEntry() { + if (mMainKeyboardView.isShifted()) { + if (!isCapsLockOn() && !mCapCharacters && !mCapWords) { + setShiftState(LeanbackKeyboardView.SHIFT_OFF); + } + } else if (isCapsLockOn() || mCapCharacters || mCapWords) { + setShiftState(LeanbackKeyboardView.SHIFT_ON); + } + } + + public void onStartInput(EditorInfo info) { + setImeOptions(mContext.getResources(), info); + mVoiceOn = false; + mLabel = LeanbackUtils.getEditorLabel(info); + } + + @SuppressLint("NewApi") + public void onStartInputView() { + clearSuggestions(); + LayoutParams params = (LayoutParams) mKeyboardsContainer.getLayoutParams(); + if (mSuggestionsEnabled) { + params.removeRule(RelativeLayout.ALIGN_PARENT_TOP); + mSuggestionsContainer.setVisibility(View.VISIBLE); + mSuggestionsBg.setVisibility(View.VISIBLE); + } else { + params.addRule(RelativeLayout.ALIGN_PARENT_TOP); + mSuggestionsContainer.setVisibility(View.GONE); + mSuggestionsBg.setVisibility(View.GONE); + } + + mKeyboardsContainer.setLayoutParams(params); + mMainKeyboardView.setKeyboard(mInitialMainKeyboard); + mVoiceButtonView.setMicEnabled(mVoiceEnabled); + resetVoice(); + dismissMiniKeyboard(); + if (!TextUtils.isEmpty(mEnterKeyText)) { + mActionButtonView.setText(mEnterKeyText); + mActionButtonView.setContentDescription(mEnterKeyText); + } else { + mActionButtonView.setText(mEnterKeyTextResId); + mActionButtonView.setContentDescription(mContext.getString(mEnterKeyTextResId)); + } + + if (mCapCharacters) { + setShiftState(LeanbackKeyboardView.SHIFT_LOCKED); + } else if (!mCapSentences && !mCapWords) { + setShiftState(LeanbackKeyboardView.SHIFT_OFF); + } else { + setShiftState(LeanbackKeyboardView.SHIFT_ON); + } + } + + public void onTextEntry() { + if (mMainKeyboardView.isShifted()) { + if (!isCapsLockOn() && !mCapCharacters) { + setShiftState(LeanbackKeyboardView.SHIFT_OFF); + } + } else if (isCapsLockOn() || mCapCharacters) { + setShiftState(LeanbackKeyboardView.SHIFT_LOCKED); + } + + if (dismissMiniKeyboard()) { + moveFocusToIndex(mMiniKbKeyIndex, KeyFocus.TYPE_MAIN); + } + + } + + public void onVoiceClick() { + if (mVoiceButtonView != null) { + mVoiceButtonView.onClick(); + } + + } + + public void resetFocusCursor() { + offsetRect(mRect, mMainKeyboardView); + mX = (float) ((double) mRect.left + (double) mRect.width() * 0.45D); + mY = (float) ((double) mRect.top + (double) mRect.height() * 0.375D); + getBestFocus(mX, mY, mTempKeyInfo); + setKbFocus(mTempKeyInfo, true, false); + setTouchStateInternal(0); + mSelectorAnimator.reverse(); + mSelectorAnimator.end(); + } + + public void resetVoice() { + mMainKeyboardView.setAlpha(mAlphaIn); + mActionButtonView.setAlpha(mAlphaIn); + mVoiceButtonView.setAlpha(mAlphaOut); + mMainKeyboardView.setVisibility(View.VISIBLE); + mActionButtonView.setVisibility(View.VISIBLE); + mVoiceButtonView.setVisibility(View.INVISIBLE); + } + + public void setDismissListener(DismissListener listener) { + mDismissListener = listener; + } + + public void setFocus(KeyFocus focus) { + setKbFocus(focus, false, true); + } + + public void setFocus(KeyFocus focus, final boolean animate) { + setKbFocus(focus, false, animate); + } + + /** + * NOTE: Draw selection over the focused key.
+ * Show selection animation when moving from one button to another. + */ + public void setSelectorToFocus(Rect rect, boolean overestimateWidth, boolean overestimateHeight, boolean animate) { + if (mSelector.getWidth() != 0 && mSelector.getHeight() != 0 && rect.width() != 0 && rect.height() != 0) { + final float width = (float) rect.width(); + final float height = (float) rect.height(); + float heightOver = height; + if (overestimateHeight) { + heightOver = height * mOverestimate; + } + + float widthOver = width; + if (overestimateWidth) { + widthOver = width * mOverestimate; + } + + float deltaY = heightOver; + float deltaX = widthOver; + float maxDelta = Math.max(widthOver, heightOver); + float minDelta = Math.min(widthOver, heightOver); + if ((double) (maxDelta / minDelta) < 1.1D) { + deltaY = maxDelta; + deltaX = maxDelta; + } + + final float x = rect.exactCenterX() - deltaX / 2.0F; + final float y = rect.exactCenterY() - deltaY / 2.0F; + mSelectorAnimation.cancel(); + + // Fix 9-patch stretching for square keys (especially on large keyboard). + if (Math.abs(deltaX - deltaY) < 1) { // is square + mKeySelector.setBackground(mKeySelectorSquare); + } else { + mKeySelector.setBackground(mKeySelectorStretched); + } + + if (animate) { + mSelectorAnimation.reset(); + mSelectorAnimation.setAnimationBounds(x, y, deltaX, deltaY); + mSelector.startAnimation(mSelectorAnimation); + } else { + mSelectorAnimation.setValues(x, y, deltaX, deltaY); + } + } + } + + /** + * Set touch state + * @param state state e.g. {@link LeanbackKeyboardContainer#TOUCH_STATE_CLICK LeanbackKeyboardContainer.TOUCH_STATE_CLICK} + */ + public void setTouchState(final int state) { + switch (state) { + case TOUCH_STATE_NO_TOUCH: + if (mTouchState == TOUCH_STATE_TOUCH_MOVE || mTouchState == TOUCH_STATE_CLICK) { + mSelectorAnimator.reverse(); + } + break; + case TOUCH_STATE_TOUCH_SNAP: + if (mTouchState == TOUCH_STATE_CLICK) { + mSelectorAnimator.reverse(); + } else if (mTouchState == TOUCH_STATE_TOUCH_MOVE) { + mSelectorAnimator.reverse(); + } + break; + case TOUCH_STATE_TOUCH_MOVE: + if (mTouchState == TOUCH_STATE_NO_TOUCH || mTouchState == TOUCH_STATE_TOUCH_SNAP) { + mSelectorAnimator.start(); + } + break; + case TOUCH_STATE_CLICK: + if (mTouchState == TOUCH_STATE_NO_TOUCH || mTouchState == TOUCH_STATE_TOUCH_SNAP) { + mSelectorAnimator.start(); + } + } + + setTouchStateInternal(state); + setKbFocus(mCurrKeyInfo, true, true); + } + + public void setVoiceListener(VoiceListener listener) { + mVoiceListener = listener; + } + + public void startVoiceRecording() { + if (mVoiceEnabled) { + if (!mVoiceKeyDismissesEnabled) { + mVoiceAnimator.startEnterAnimation(); + } else { + mDismissListener.onDismiss(true); + } + } + } + + /** + * Switch to next keyboard (looped). + * {@link KeyboardManager KeyboardManager} is the source behind all keyboard implementations + */ + public void switchToNextKeyboard() { + KeyboardData nextKeyboard = mKeyboardManager.next(); + Keyboard currentKeyboard = mMainKeyboardView.getKeyboard(); + + if (currentKeyboard != null && + currentKeyboard.equals(nextKeyboard.abcKeyboard)) { // one keyboard in the list + // Prompt user to select layout. + Helpers.startActivity(mContext, KbLayoutActivity.class); + mContext.hideIme(); + } else { + mInitialMainKeyboard = nextKeyboard.abcKeyboard; + mAbcKeyboard = nextKeyboard.abcKeyboard; + mMainKeyboardView.setKeyboard(nextKeyboard.abcKeyboard); + + mSymKeyboard = nextKeyboard.symKeyboard; + mNumKeyboard = nextKeyboard.numKeyboard; + } + } + + public void updateAddonKeyboard() { + mKeyboardManager.load(); // force reload to fix such errors as invisible kbd + KeyboardData keyboard = mKeyboardManager.get(); + mInitialMainKeyboard = keyboard.abcKeyboard; + mAbcKeyboard = keyboard.abcKeyboard; + mMainKeyboardView.setKeyboard(keyboard.abcKeyboard); + + mSymKeyboard = keyboard.symKeyboard; + mNumKeyboard = keyboard.numKeyboard; + + mThemeManager.updateKeyboardTheme(); + } + + public void updateSuggestions(ArrayList suggestions) { + addUserInputToSuggestions(suggestions); + + int oldCount = mSuggestions.getChildCount(); + int newCount = suggestions.size(); + if (newCount < oldCount) { + mSuggestions.removeViews(newCount, oldCount - newCount); + } else if (newCount > oldCount) { + while (oldCount < newCount) { + View suggestion = mContext.getLayoutInflater().inflate(R.layout.candidate, null); + mSuggestions.addView(suggestion); + ++oldCount; + } + } + + for (oldCount = 0; oldCount < newCount; ++oldCount) { + Button suggestion = mSuggestions.getChildAt(oldCount).findViewById(R.id.text); + suggestion.setText(suggestions.get(oldCount)); + suggestion.setContentDescription(suggestions.get(oldCount)); + } + + if (getCurrFocus().type == KeyFocus.TYPE_SUGGESTION) { + resetFocusCursor(); + } + + mThemeManager.updateSuggestionsTheme(); + } + + /** + * Useful for password fields + */ + private void addUserInputToSuggestions(ArrayList suggestions) { + InputConnection connection = mContext.getCurrentInputConnection(); + + if (connection != null) { + String editorText = LeanbackUtils.getEditorText(connection); + + if (editorText.isEmpty()) { + editorText = mLabel; + } + + if (suggestions.size() == 0) { + suggestions.add(editorText); + } else { + suggestions.set(0, editorText); + } + } + } + + public void onLangKeyClick() { + switchToNextKeyboard(); + } + + public void onClipboardClick(InputListener listener) { + ClipboardManager clipBoard = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + + if (clipBoard != null) { + ClipData clipData = clipBoard.getPrimaryClip(); + if (clipData != null) { + ClipData.Item item = clipData.getItemAt(0); + String text = item.getText().toString(); + if (listener != null) { + listener.onEntry(InputListener.ENTRY_TYPE_STRING, LeanbackKeyboardView.NOT_A_KEY, text); + } + } + } + } + + public interface DismissListener { + void onDismiss(boolean fromVoice); + } + + public static class KeyFocus { + public static final int TYPE_ACTION = 2; + public static final int TYPE_INVALID = -1; + public static final int TYPE_MAIN = 0; + public static final int TYPE_SUGGESTION = 3; + public static final int TYPE_VOICE = 1; + int code; + int index; + CharSequence label; + final Rect rect = new Rect(); + int type = TYPE_INVALID; + + @Override + public boolean equals(Object obj) { + if (this != obj) { + if (obj == null || this.getClass() != obj.getClass()) { + return false; + } + + KeyFocus focus = (KeyFocus) obj; + + if (this.code != focus.code) { + return false; + } + + if (this.index != focus.index) { + return false; + } + + if (this.type != focus.type) { + return false; + } + + // equality must be commutative + if (this.label == null && focus.label != null) { + return false; + } + + if (this.label != null && !this.label.equals(focus.label)) { + return false; + } + + if (!this.rect.equals(focus.rect)) { + return false; + } + } + + return true; + } + + @Override + public int hashCode() { + int hash = this.rect.hashCode(); + int index = this.index; + int type = this.type; + int code = this.code; + int salt; + if (this.label != null) { + salt = this.label.hashCode(); + } else { + salt = 0; + } + + return (((hash * 31 + index) * 31 + type) * 31 + code) * 31 + salt; + } + + public void set(KeyFocus focus) { + this.index = focus.index; + this.type = focus.type; + this.code = focus.code; + this.label = focus.label; + this.rect.set(focus.rect); + } + + @Override + public String toString() { + return "[type: " + this.type + ", index: " + this.index + ", code: " + this.code + ", label: " + this.label + ", rect: " + this.rect + "]"; + } + } + + private class ScaleAnimation extends Animation { + private float mEndHeight; + private float mEndWidth; + private float mEndX; + private float mEndY; + private final ViewGroup.LayoutParams mParams; + private float mStartHeight; + private float mStartWidth; + private float mStartX; + private float mStartY; + private final View mView; + + public ScaleAnimation(FrameLayout view) { + mView = view; + mParams = view.getLayoutParams(); + setDuration(MOVEMENT_ANIMATION_DURATION); + setInterpolator(sMovementInterpolator); + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation transformation) { + if (interpolatedTime == 0.0F) { + mStartX = mView.getX(); + mStartY = mView.getY(); + mStartWidth = (float) mParams.width; + mStartHeight = (float) mParams.height; + } else { + setValues((mEndX - mStartX) * interpolatedTime + mStartX, + (mEndY - mStartY) * interpolatedTime + mStartY, + (float) ((int) ((mEndWidth - mStartWidth) * interpolatedTime + mStartWidth)), + (float) ((int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight))); + } + } + + public void setAnimationBounds(float x, float y, float width, float height) { + mEndX = x; + mEndY = y; + mEndWidth = width; + mEndHeight = height; + } + + public void setValues(float x, float y, float width, float height) { + mView.setX(x); + mView.setY(y); + mParams.width = (int) width; + mParams.height = (int) height; + mView.setLayoutParams(mParams); + mView.requestLayout(); + } + } + + private class VoiceIntroAnimator { + private AnimatorListener mEnterListener; + private AnimatorListener mExitListener; + private ValueAnimator mValueAnimator; + + public VoiceIntroAnimator(AnimatorListener enterListener, AnimatorListener exitListener) { + mEnterListener = enterListener; + mExitListener = exitListener; + mValueAnimator = ValueAnimator.ofFloat(mAlphaOut, mAlphaIn); + mValueAnimator.setDuration(mVoiceAnimDur); + mValueAnimator.setInterpolator(new AccelerateInterpolator()); + } + + private void start(final boolean enterVoice) { + mValueAnimator.cancel(); + mValueAnimator.removeAllListeners(); + + AnimatorListener listener; + + if (enterVoice) { + listener = mEnterListener; + } else { + listener = mExitListener; + } + + mValueAnimator.addListener(listener); + mValueAnimator.removeAllUpdateListeners(); + mValueAnimator.addUpdateListener(animation -> { + float scale = (Float) mValueAnimator.getAnimatedValue(); + float calcOpacity = mAlphaIn + mAlphaOut - scale; + float opacity; + if (enterVoice) { + opacity = calcOpacity; + } else { + opacity = scale; + } + + if (enterVoice) { + calcOpacity = scale; + } + + mMainKeyboardView.setAlpha(opacity); + mActionButtonView.setAlpha(opacity); + mVoiceButtonView.setAlpha(calcOpacity); + if (scale == mAlphaOut) { + if (!enterVoice) { + mMainKeyboardView.setVisibility(View.VISIBLE); + mActionButtonView.setVisibility(View.VISIBLE); + return; + } + + mVoiceButtonView.setVisibility(View.VISIBLE); + } else if (scale == mAlphaIn) { + if (enterVoice) { + mMainKeyboardView.setVisibility(View.INVISIBLE); + mActionButtonView.setVisibility(View.INVISIBLE); + return; + } + + mVoiceButtonView.setVisibility(View.INVISIBLE); + } + }); + mValueAnimator.start(); + } + + void startEnterAnimation() { + if (!isVoiceVisible() && !mValueAnimator.isRunning()) { + start(true); + } + } + + void startExitAnimation() { + if (isVoiceVisible() && !mValueAnimator.isRunning()) { + start(false); + } + } + } + + public interface VoiceListener { + void onVoiceResult(String result); + } + + private class MyVoiceRecognitionListener implements RecognitionListener { + float peakRmsLevel = 0.0F; + int rmsCounter = 0; + + @Override + public void onBeginningOfSpeech() { + mVoiceButtonView.showRecording(); + } + + @Override + public void onBufferReceived(byte[] buffer) { + // NOP + } + + @Override + public void onEndOfSpeech() { + mVoiceButtonView.showRecognizing(); + mVoiceOn = false; + } + + @Override + public void onError(int error) { + cancelVoiceRecording(); + + String errorMsg; + + switch (error) { + case SpeechRecognizer.ERROR_SERVER: + errorMsg = "recognizer error server error"; + break; + case SpeechRecognizer.ERROR_CLIENT: + errorMsg = "recognizer error client error"; + break; + case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + errorMsg = "recognizer error speech timeout"; + break; + case SpeechRecognizer.ERROR_NO_MATCH: + errorMsg = "recognizer error no match"; + break; + default: + errorMsg = "recognizer other error " + error; + } + + MessageHelpers.showLongMessage(mContext, errorMsg); + + Log.d(TAG, errorMsg); + } + + @Override + public void onEvent(int eventType, Bundle bundle) { + // NOP + } + + @Override + public void onPartialResults(Bundle bundle) { + // NOP + } + + @Override + public void onReadyForSpeech(Bundle bundle) { + mVoiceButtonView.showListening(); + } + + @Override + public void onResults(Bundle bundle) { + List results = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + + if (results != null && mVoiceListener != null) { + mVoiceListener.onVoiceResult(results.get(0)); + } + + cancelVoiceRecording(); + } + + // TODO: not fully decompiled and may contains bugs + @Override + public void onRmsChanged(float rmsdB) { + synchronized (this) { + mVoiceOn = true; + + int speechLevel = 0; + + if (rmsdB >= 0) { + speechLevel = (int) (rmsdB * 10f); + } + + mSpeechLevelSource.setSpeechLevel(speechLevel); + + peakRmsLevel = Math.max(peakRmsLevel, rmsdB); + rmsCounter++; + + if (rmsCounter <= 100) { + return; + } + + if (peakRmsLevel < 0) { + return; + } + + mVoiceButtonView.showNotListening(); + } + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardController.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardController.java new file mode 100644 index 0000000..2606039 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardController.java @@ -0,0 +1,955 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.graphics.PointF; +import android.inputmethodservice.InputMethodService; +import android.inputmethodservice.Keyboard.Key; +import android.os.Handler; +import android.text.InputType; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnHoverListener; +import android.view.View.OnLayoutChangeListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardContainer.KeyFocus; +import com.liskovsoft.leankeyboard.ime.pano.util.TouchNavSpaceTracker; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.ArrayList; + +public class LeanbackKeyboardController implements LeanbackKeyboardContainer.VoiceListener, + LeanbackKeyboardContainer.DismissListener, + OnTouchListener, OnHoverListener, Runnable { + public static final int CLICK_MOVEMENT_BLOCK_DURATION_MS = 500; + private static final boolean DEBUG = false; + private static final int KEY_CHANGE_HISTORY_SIZE = 10; + private static final long KEY_CHANGE_REVERT_TIME_MS = 100L; + private static final String TAG = "LbKbController"; + public static final String TAG_GO = "Go"; + private boolean mClickConsumed; + private long mLastClickTime; + private LeanbackKeyboardContainer mContainer; + private InputMethodService mContext; + private DoubleClickDetector mDoubleClickDetector; + private LeanbackKeyboardContainer.KeyFocus mCurrentFocus; + private Handler mHandler; + private InputListener mInputListener; + ArrayList mKeyChangeHistory; + private LeanbackKeyboardContainer.KeyFocus mKeyDownKeyFocus; + private boolean mKeyDownReceived; + private boolean mLongPressHandled; + private int mMoveCount; + private OnLayoutChangeListener mOnLayoutChangeListener; + public float mResizeSquareDistance; + private TouchNavSpaceTracker mSpaceTracker; + private LeanbackKeyboardContainer.KeyFocus mTempFocus; + private PointF mTempPoint; + private LeanbackKeyboardController.TouchEventListener mTouchEventListener; + private long mPrevTime; + private boolean mShowInput; + private int mLastEditorIdPhysicalKeyboardWasUsed; + private boolean mHideKeyboardWhenPhysicalKeyboardUsed = true; + + public LeanbackKeyboardController(final InputMethodService context, + final InputListener listener) { + this(context, listener, new TouchNavSpaceTracker(), new LeanbackKeyboardContainer(context)); + } + + public LeanbackKeyboardController(final InputMethodService context, + final InputListener listener, + final TouchNavSpaceTracker tracker, + final LeanbackKeyboardContainer container) { + mDoubleClickDetector = new DoubleClickDetector(); + mOnLayoutChangeListener = (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + left = right - left; + top = bottom - top; + if (left > 0 && top > 0) { + if (mSpaceTracker != null) { + mSpaceTracker.setPixelSize((float) left, (float) top); + } + + if (left != oldRight - oldLeft || top != oldBottom - oldTop) { + initInputView(); + } + } + + }; + mTouchEventListener = new TouchEventListener(); + mCurrentFocus = new KeyFocus(); + mTempFocus = new KeyFocus(); + mKeyChangeHistory = new ArrayList<>(11); + mTempPoint = new PointF(); + mKeyDownReceived = false; + mLongPressHandled = false; + mContext = context; + mResizeSquareDistance = context.getResources().getDimension(R.dimen.resize_move_distance); + mResizeSquareDistance *= mResizeSquareDistance; + mInputListener = listener; + setSpaceTracker(tracker); + setKeyboardContainer(container); + mContainer.setVoiceListener(this); + mContainer.setDismissListener(this); + } + + private boolean applyLETVFixesDown(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + return true; + default: + return false; + } + } + + private boolean applyLETVFixesUp(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + mContainer.switchToNextKeyboard(); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + fakeKeyIndex(0, KeyFocus.TYPE_ACTION); + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + fakeKeyCode(LeanbackKeyboardView.KEYCODE_DELETE); + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + fakeKeyCode(LeanbackKeyboardView.ASCII_SPACE); + break; + default: + return false; + } + + return true; + } + + private void beginLongClickCountdown() { + this.mClickConsumed = false; + Handler handler = this.mHandler; + if (handler == null) { + handler = new Handler(); + this.mHandler = handler; + } + + handler.removeCallbacks(this); + handler.postDelayed(this, (long) 1000); + } + + private void clearKeyIfNecessary() { + ++mMoveCount; + if (mMoveCount >= 3) { + mMoveCount = 0; + mKeyDownKeyFocus = null; + } + + } + + private void commitKey() { + this.commitKey(this.mContainer.getCurrFocus()); + } + + /** + * NOTE: Where all magic happens. Input from virtual kbd is processed here. + * @param focus current key + */ + private void commitKey(KeyFocus focus) { + if (mContainer != null && focus != null) { + switch (focus.type) { + case KeyFocus.TYPE_VOICE: + mContainer.onVoiceClick(); + return; + case KeyFocus.TYPE_ACTION: // User presses Go, Send, Search etc + mInputListener.onEntry(InputListener.ENTRY_TYPE_ACTION, 0, null); + // mContext.hideWindow(); // SmartYouTubeTV fix: force hide keyboard + return; + case KeyFocus.TYPE_SUGGESTION: + mInputListener.onEntry(InputListener.ENTRY_TYPE_SUGGESTION, 0, mContainer.getSuggestionText(focus.index)); + return; + default: + Key key = mContainer.getKey(focus.type, focus.index); + if (key != null) { + handleCommitKeyboardKey(key.codes[0], key.label); + } + } + } + } + + private void fakeClickDown() { + mContainer.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_CLICK); + } + + private void fakeClickUp() { + LeanbackKeyboardContainer container = mContainer; + commitKey(container.getCurrFocus()); + container.setTouchState(0); + } + + private void fakeKeyCode(final int keyCode) { + mContainer.getCurrFocus().code = keyCode; + handleCommitKeyboardKey(keyCode, null); + } + + /** + * Fake key index + * @param index key index + * @param type {@link KeyFocus KeyFocus} constant + */ + private void fakeKeyIndex(final int index, final int type) { + LeanbackKeyboardContainer.KeyFocus focus = mContainer.getCurrFocus(); + focus.index = index; + focus.type = type; + commitKey(focus); + } + + private void fakeLongClickDown() { + LeanbackKeyboardContainer container = mContainer; + container.onKeyLongPress(); + container.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_CLICK); + } + + private void fakeLongClickUp() { + this.mContainer.setTouchState(0); + } + + private PointF getBestSnapPosition(final PointF currPoint, final long currTime) { + if (mKeyChangeHistory.size() <= 1) { + return currPoint; + } else { + int count = 0; + + PointF pos; + while (true) { + pos = currPoint; + if (count >= mKeyChangeHistory.size() - 1) { + break; + } + + LeanbackKeyboardController.KeyChange change = mKeyChangeHistory.get(count); + if (currTime - mKeyChangeHistory.get(count + 1).time < KEY_CHANGE_REVERT_TIME_MS) { + pos = change.position; + mKeyChangeHistory.clear(); + mKeyChangeHistory.add(new LeanbackKeyboardController.KeyChange(currTime, pos)); + break; + } + + ++count; + } + + return pos; + } + } + + private PointF getCurrentKeyPosition() { + if (mContainer != null) { + LeanbackKeyboardContainer.KeyFocus focus = mContainer.getCurrFocus(); + return new PointF((float) focus.rect.centerX(), (float) focus.rect.centerY()); + } else { + return null; + } + } + + private PointF getRelativePosition(View view, MotionEvent event) { + int[] location = new int[2]; + view.getLocationOnScreen(location); + float x = event.getRawX(); + float y = event.getRawY(); + return new PointF(x - (float) location[0], y - (float) location[1]); + } + + private int getSimplifiedKey(final int keyCode) { + int defaultCode = KeyEvent.KEYCODE_DPAD_CENTER; + if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER) { + final byte enter = KeyEvent.KEYCODE_ENTER; + defaultCode = enter; + if (keyCode != KeyEvent.KEYCODE_ENTER) { + defaultCode = enter; + if (keyCode != KeyEvent.KEYCODE_NUMPAD_ENTER) { + defaultCode = keyCode; + if (keyCode == KeyEvent.KEYCODE_BUTTON_A) { + defaultCode = enter; + } + } + } + } + + if (defaultCode == KeyEvent.KEYCODE_BUTTON_B) { + defaultCode = KeyEvent.KEYCODE_BACK; + } + + return defaultCode; + } + + /** + * NOTE: Specials keys (e.g. voice key) handled here + * @param keyCode key code e.g {@link LeanbackKeyboardView#KEYCODE_SHIFT LeanbackKeyboardView.KEYCODE_SHIFT} + * @param text typed content + */ + private void handleCommitKeyboardKey(int keyCode, CharSequence text) { + switch (keyCode) { + case LeanbackKeyboardView.KEYCODE_DISMISS_MINI_KEYBOARD: + mContainer.dismissMiniKeyboard(); + return; + case LeanbackKeyboardView.KEYCODE_VOICE: + mContainer.startVoiceRecording(); + return; + case LeanbackKeyboardView.KEYCODE_CAPS_LOCK: + mContainer.onShiftDoubleClick(mContainer.isCapsLockOn()); + return; + case LeanbackKeyboardView.KEYCODE_DELETE: + mInputListener.onEntry(InputListener.ENTRY_TYPE_BACKSPACE, LeanbackKeyboardView.SHIFT_OFF, null); + return; + case LeanbackKeyboardView.KEYCODE_RIGHT: + mInputListener.onEntry(InputListener.ENTRY_TYPE_RIGHT, LeanbackKeyboardView.SHIFT_OFF, null); + return; + case LeanbackKeyboardView.KEYCODE_LEFT: + mInputListener.onEntry(InputListener.ENTRY_TYPE_LEFT, LeanbackKeyboardView.SHIFT_OFF, null); + return; + case LeanbackKeyboardView.KEYCODE_SYM_TOGGLE: + if (Log.isLoggable("LbKbController", Log.DEBUG)) { + Log.d("LbKbController", "mode change"); + } + + mContainer.onModeChangeClick(); + return; + case LeanbackKeyboardView.KEYCODE_SHIFT: + if (Log.isLoggable("LbKbController", Log.DEBUG)) { + Log.d("LbKbController", "shift"); + } + + mContainer.onShiftClick(); + return; + case LeanbackKeyboardView.ASCII_SPACE: + mInputListener.onEntry(InputListener.ENTRY_TYPE_STRING, keyCode, " "); + mContainer.onSpaceEntry(); + return; + case LeanbackKeyboardView.ASCII_PERIOD: + mInputListener.onEntry(InputListener.ENTRY_TYPE_STRING, keyCode, text); + mContainer.onPeriodEntry(); + return; + case LeanbackKeyboardView.KEYCODE_LANG_TOGGLE: + if (Log.isLoggable("LbKbController", Log.DEBUG)) { + Log.d("LbKbController", "language change"); + } + + mContainer.onLangKeyClick(); + return; + case LeanbackKeyboardView.KEYCODE_CLIPBOARD: + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "paste from clipboard"); + } + + mContainer.onClipboardClick(mInputListener); + return; + default: + mInputListener.onEntry(InputListener.ENTRY_TYPE_STRING, keyCode, text); + mContainer.onTextEntry(); + if (mContainer.isMiniKeyboardOnScreen()) { + mContainer.dismissMiniKeyboard(); + } + + } + } + + private boolean handleKeyDownEvent(int keyCode, int eventRepeatCount) { + keyCode = getSimplifiedKey(keyCode); + boolean handled; + if (keyCode == KeyEvent.KEYCODE_BACK) { + mContainer.cancelVoiceRecording(); + handled = false; + } else if (mContainer.isVoiceVisible()) { + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { + mContainer.cancelVoiceRecording(); + } + + handled = true; + } else { + handled = true; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_UP); + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_DOWN); + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = onDirectionalMove(LeanbackKeyboardContainer.DIRECTION_RIGHT); + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (eventRepeatCount == 0) { + mMoveCount = 0; + mKeyDownKeyFocus = new LeanbackKeyboardContainer.KeyFocus(); + mKeyDownKeyFocus.set(mContainer.getCurrFocus()); + } else if (eventRepeatCount == 1 && handleKeyLongPress(keyCode)) { // space long press handler and others + mKeyDownKeyFocus = null; + } + + handled = true; + if (isKeyHandledOnKeyDown(mContainer.getCurrKeyCode())) { + commitKey(); + handled = true; + } + break; + case KeyEvent.KEYCODE_BUTTON_X: + handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_DELETE, null); + handled = true; + break; + case KeyEvent.KEYCODE_BUTTON_Y: + handleCommitKeyboardKey(LeanbackKeyboardView.ASCII_SPACE, null); + handled = true; + break; + case KeyEvent.KEYCODE_BUTTON_L1: + handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_LEFT, null); + handled = true; + break; + case KeyEvent.KEYCODE_BUTTON_R1: + handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_RIGHT, null); + handled = true; + case KeyEvent.KEYCODE_BUTTON_THUMBL: + case KeyEvent.KEYCODE_BUTTON_THUMBR: + break; + default: + handled = false; + } + } + + if (!handled) { + handled = applyLETVFixesDown(keyCode); + } + + return handled; + } + + private boolean handleKeyLongPress(int keyCode) { + mLongPressHandled = isEnterKey(keyCode) && mContainer.onKeyLongPress(); + + if (mContainer.isMiniKeyboardOnScreen()) { + Log.d(TAG, "mini keyboard shown after long press"); + } + + return mLongPressHandled; + } + + private boolean handleKeyUpEvent(int keyCode, long currTime) { + keyCode = getSimplifiedKey(keyCode); + boolean handled; + + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + handled = false; + } else if (mContainer.isVoiceVisible()) { + handled = true; + } else { + handled = true; + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + clearKeyIfNecessary(); + handled = true; + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (mContainer.getCurrKeyCode() == LeanbackKeyboardView.KEYCODE_SHIFT) { + mDoubleClickDetector.addEvent(currTime); + handled = true; + } else { + handled = true; + if (!isKeyHandledOnKeyDown(mContainer.getCurrKeyCode())) { + commitKey(mKeyDownKeyFocus); + handled = true; + } + } + case KeyEvent.KEYCODE_BUTTON_X: + case KeyEvent.KEYCODE_BUTTON_Y: + case KeyEvent.KEYCODE_BUTTON_L1: + case KeyEvent.KEYCODE_BUTTON_R1: + break; + case KeyEvent.KEYCODE_BUTTON_THUMBL: + handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_SYM_TOGGLE, null); + handled = true; + break; + case KeyEvent.KEYCODE_BUTTON_THUMBR: + handleCommitKeyboardKey(LeanbackKeyboardView.KEYCODE_CAPS_LOCK, null); + handled = true; + break; + case KeyEvent.KEYCODE_VOICE_ASSIST: + case KeyEvent.KEYCODE_SEARCH: + mContainer.startVoiceRecording(); + handled = true; + break; + default: + handled = false; + } + } + + if (!handled) { + handled = applyLETVFixesUp(keyCode); + } + + return handled; + } + + private void initInputView() { + mContainer.onInitInputView(); + updatePositionToCurrentFocus(); + } + + /** + * Simple throttle routine. + * @param callInterval interval + * @return is allowed + */ + private boolean isCallAllowedOrigin(int callInterval) { + long currTimeMS = System.currentTimeMillis(); + long timeDelta = currTimeMS - mPrevTime; + if (mPrevTime != 0 && timeDelta <= (callInterval * 3)) { + if (timeDelta > callInterval) { + mPrevTime = 0; + return true; + } + } else { + mPrevTime = currTimeMS; + } + + return false; + } + + /** + * Simple throttle routine. Simplified comparing to previous. Not tested yet!!!! + * @param interval interval + * @return is allowed + */ + private boolean isCallAllowed2(int interval) { + long currTimeMS = System.currentTimeMillis(); + long timeDelta = currTimeMS - mPrevTime; + if (mPrevTime == 0) { + mPrevTime = currTimeMS; + return true; + } else if (timeDelta > interval) { + mPrevTime = 0; + } + + return false; + } + + private boolean isDoubleClick() { + long currTimeMS = System.currentTimeMillis(); + long lastTime = mLastClickTime; + if (mLastClickTime != 0L && currTimeMS - lastTime <= (long) 300) { + return true; + } else { + mLastClickTime = currTimeMS; + return false; + } + } + + private boolean isEnterKey(int keyCode) { + keyCode = getSimplifiedKey(keyCode); + return keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER; + } + + /** + * Whether key down is handled + * @param currKeyCode key code e.g. {@link LeanbackKeyboardView#KEYCODE_DELETE LeanbackKeyboardView.KEYCODE_DELETE} + * @return key down is handled + */ + private boolean isKeyHandledOnKeyDown(int currKeyCode) { + return currKeyCode == LeanbackKeyboardView.KEYCODE_DELETE || currKeyCode == LeanbackKeyboardView.KEYCODE_LEFT || currKeyCode == LeanbackKeyboardView.KEYCODE_RIGHT; + } + + private void moveSelectorToPoint(float x, float y) { + LeanbackKeyboardContainer container = mContainer; + LeanbackKeyboardContainer.KeyFocus focus = mTempFocus; + container.getBestFocus(x, y, focus); + mContainer.setFocus(mTempFocus, false); + } + + private boolean onDirectionalMove(int dir) { + if (mContainer.getNextFocusInDirection(dir, mCurrentFocus, mTempFocus)) { + mContainer.updateCyclicFocus(dir, mCurrentFocus, mTempFocus); + mContainer.setFocus(mTempFocus); + mCurrentFocus.set(mTempFocus); + clearKeyIfNecessary(); + } + + return true; + } + + private void performBestSnap(long time) { + LeanbackKeyboardContainer.KeyFocus focus = mContainer.getCurrFocus(); + mTempPoint.x = (float) focus.rect.centerX(); + mTempPoint.y = (float) focus.rect.centerY(); + PointF pos = getBestSnapPosition(mTempPoint, time); + mContainer.getBestFocus(pos.x, pos.y, mTempFocus); + mContainer.setFocus(mTempFocus); + updatePositionToCurrentFocus(); + } + + /** + * Set key state + * @param keyIndex key index + * @param keyState constant e.g. {@link LeanbackKeyboardContainer#TOUCH_STATE_CLICK LeanbackKeyboardContainer.TOUCH_STATE_CLICK} + */ + private void setKeyState(int keyIndex, boolean keyState) { + LeanbackKeyboardContainer container = this.mContainer; + LeanbackKeyboardContainer.KeyFocus focus = container.getCurrFocus(); + focus.index = keyIndex; + focus.type = KeyFocus.TYPE_MAIN; + byte state; + if (keyState) { + state = LeanbackKeyboardContainer.TOUCH_STATE_CLICK; + } else { + state = LeanbackKeyboardContainer.TOUCH_STATE_NO_TOUCH; + } + + container.setTouchState(state); + } + + private void updatePositionToCurrentFocus() { + PointF pos = getCurrentKeyPosition(); + if (pos != null && mSpaceTracker != null) { + mSpaceTracker.setPixelPosition(pos.x, pos.y); + } + + } + + public boolean areSuggestionsEnabled() { + return mContainer != null && mContainer.areSuggestionsEnabled(); + } + + public void setSuggestionsEnabled(boolean enabled) { + if (mContainer != null) { + mContainer.setSuggestionsEnabled(enabled); + } + } + + public boolean enableAutoEnterSpace() { + return mContainer != null && mContainer.enableAutoEnterSpace(); + } + + public View getView() { + if (mContainer != null) { + RelativeLayout view = mContainer.getView(); + view.setClickable(true); + view.setOnTouchListener(this); + view.setOnHoverListener(this); + Button button = mContainer.getGoButton(); + button.setOnTouchListener(this); + button.setOnHoverListener(this); + button.setTag(TAG_GO); + return view; + } else { + return null; + } + } + + public void onDismiss(boolean fromVoice) { + if (fromVoice) { + mInputListener.onEntry(InputListener.ENTRY_TYPE_VOICE_DISMISS, LeanbackKeyboardView.SHIFT_OFF, null); + } else { + mInputListener.onEntry(InputListener.ENTRY_TYPE_DISMISS, LeanbackKeyboardView.SHIFT_OFF, null); + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + return mSpaceTracker != null && mContext != null && mContext.isInputViewShown() && mSpaceTracker.onGenericMotionEvent(event); + } + + /** + * Control keyboard from the mouse. Movement catching + * @param view active view + * @param event event object + * @return is hover handled + */ + @Override + public boolean onHover(View view, MotionEvent event) { + boolean handled = false; + if (event.getAction() == MotionEvent.ACTION_HOVER_MOVE) { + PointF pos = getRelativePosition(mContainer.getView(), event); + moveSelectorToPoint(pos.x, pos.y); + handled = true; + } + + return handled; + } + + /** + * Try to handle key down event + * @param keyCode key code e.g. {@link KeyEvent#KEYCODE_ENTER KeyEvent.KEYCODE_ENTER} + * @param event {@link KeyEvent KeyEvent} + * @return is event handled + */ + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + //greater than zero means it is a physical keyboard. + //we also want to hide the view if it's a glyph (for example, not physical volume-up key) + //if (event.getDeviceId() > 0 && event.isPrintingKey()) onPhysicalKeyboardKeyPressed(); + if (event.isPrintingKey()) onPhysicalKeyboardKeyPressed(); + + mCurrentFocus.set(mContainer.getCurrFocus()); + if (mSpaceTracker != null && mSpaceTracker.onKeyDown(keyCode, event)) { + return true; + } else { + if (isEnterKey(keyCode)) { + mKeyDownReceived = true; + if (event.getRepeatCount() == 0) { + mContainer.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_CLICK); + } + } + + return handleKeyDownEvent(keyCode, event.getRepeatCount()); + } + } + + private void onPhysicalKeyboardKeyPressed() { + EditorInfo editorInfo = mContext.getCurrentInputEditorInfo(); + mLastEditorIdPhysicalKeyboardWasUsed = editorInfo == null ? 0 : editorInfo.fieldId; + if (mHideKeyboardWhenPhysicalKeyboardUsed) { + mContext.hideWindow(); + } + + // For all other keys, if we want to do transformations on + // text being entered with a hard keyboard, we need to process + // it and do the appropriate action. + // using physical keyboard is more annoying with candidate view in + // the way + // so we disable it. + + // stopping any soft-keyboard prediction + //abortCorrectionAndResetPredictionState(false); + } + + public boolean onKeyUp(int keyCode, KeyEvent keyEvent) { + if (mSpaceTracker != null && mSpaceTracker.onKeyUp(keyCode, keyEvent)) { + return true; + } else { + if (isEnterKey(keyCode)) { + if (!mKeyDownReceived || mLongPressHandled) { + mLongPressHandled = false; + return true; + } + + mKeyDownReceived = false; + if (mContainer.getTouchState() == 3) { + mContainer.setTouchState(1); + } + } + + return handleKeyUpEvent(keyCode, keyEvent.getEventTime()); + } + } + + public void onStartInput(EditorInfo info) { + if (mContainer != null) { + mContainer.onStartInput(info); + initInputView(); + } + + //// prevent accidental kbd pop-up on FireTV devices + //// more info: https://forum.xda-developers.com/fire-tv/general/guide-change-screen-keyboard-to-leankey-t3527675/page2 + //int maskAction = info.imeOptions & EditorInfo.IME_MASK_ACTION; + //mShowInput = maskAction != 0; + + mShowInput = info.inputType != InputType.TYPE_NULL; + } + + public boolean showInputView() { + return mShowInput; + } + + private void onHideIme() { + mContext.requestHideSelf(InputMethodService.BACK_DISPOSITION_DEFAULT); + } + + public void onStartInputView() { + mKeyDownReceived = false; + + if (mContainer != null) { + mContainer.onStartInputView(); + } + + mDoubleClickDetector.reset(); + } + + @Override + public boolean onTouch(View view, MotionEvent event) { + Object tag = view.getTag(); + boolean isEnterKey = TAG_GO.equals(tag); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (isEnterKey) { + break; + } + + moveSelectorToPoint(event.getX(), event.getY()); + fakeClickDown(); + beginLongClickCountdown(); + break; + case MotionEvent.ACTION_UP: + if (isEnterKey) { + fakeKeyIndex(0, KeyFocus.TYPE_ACTION); + break; + } + + if (!mClickConsumed) { + mClickConsumed = true; + if (isDoubleClick()) { + mContainer.onKeyLongPress(); + break; + } + + fakeClickUp(); + } + + fakeLongClickUp(); + break; + default: + return false; + } + + return true; + } + + @Override + public void onVoiceResult(String result) { + mInputListener.onEntry(InputListener.ENTRY_TYPE_VOICE, 0, result); + } + + @Override + public void run() { + if (!mClickConsumed) { + mClickConsumed = true; + fakeLongClickDown(); + } + } + + public void setKeyboardContainer(LeanbackKeyboardContainer container) { + mContainer = container; + container.getView().addOnLayoutChangeListener(mOnLayoutChangeListener); + } + + public void setSpaceTracker(TouchNavSpaceTracker tracker) { + mSpaceTracker = tracker; + tracker.setLPFEnabled(true); + tracker.setKeyEventListener(mTouchEventListener); + } + + public void initKeyboards() { + mContainer.initKeyboards(); + } + + public void updateSuggestions(ArrayList suggestions) { + if (mContainer != null) { + mContainer.updateSuggestions(suggestions); + } + + } + + public void setHideWhenPhysicalKeyboardUsed(boolean hide) { + mHideKeyboardWhenPhysicalKeyboardUsed = hide; + } + + private class DoubleClickDetector { + final long DOUBLE_CLICK_TIMEOUT_MS; + boolean mFirstClickShiftLocked; + long mFirstClickTime; + + private DoubleClickDetector() { + DOUBLE_CLICK_TIMEOUT_MS = 200L; + mFirstClickTime = 0L; + } + + public void addEvent(long currTime) { + if (currTime - mFirstClickTime > DOUBLE_CLICK_TIMEOUT_MS) { + mFirstClickTime = currTime; + mFirstClickShiftLocked = mContainer.isCapsLockOn(); + commitKey(); + } else { + mContainer.onShiftDoubleClick(mFirstClickShiftLocked); + reset(); + } + } + + public void reset() { + mFirstClickTime = 0L; + } + } + + public interface InputListener { + int ENTRY_TYPE_ACTION = 5; + int ENTRY_TYPE_BACKSPACE = 1; + int ENTRY_TYPE_DISMISS = 7; + int ENTRY_TYPE_LEFT = 3; + int ENTRY_TYPE_RIGHT = 4; + int ENTRY_TYPE_STRING = 0; + int ENTRY_TYPE_SUGGESTION = 2; + int ENTRY_TYPE_VOICE = 6; + int ENTRY_TYPE_VOICE_DISMISS = 8; + + /** + * User has typed something + * @param type type e.g. {@link InputListener#ENTRY_TYPE_ACTION InputListener.ENTRY_TYPE_ACTION} + * @param keyCode key code e.g. {@link LeanbackKeyboardView#SHIFT_ON LeanbackKeyboardView.SHIFT_ON} + * @param text text + */ + void onEntry(int type, int keyCode, CharSequence text); + } + + private static final class KeyChange { + public PointF position; + public long time; + + public KeyChange(long time, PointF position) { + this.time = time; + this.position = position; + } + } + + private class TouchEventListener implements TouchNavSpaceTracker.KeyEventListener { + private TouchEventListener() { + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (isEnterKey(keyCode)) { + mKeyDownReceived = true; + if (event.getRepeatCount() == 0) { + mContainer.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_CLICK); + mSpaceTracker.blockMovementUntil(event.getEventTime() + CLICK_MOVEMENT_BLOCK_DURATION_MS); + performBestSnap(event.getEventTime()); + } + } + + return handleKeyDownEvent(keyCode, event.getRepeatCount()); + } + + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return handleKeyLongPress(keyCode); + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (isEnterKey(keyCode)) { + if (!mKeyDownReceived || mLongPressHandled) { + mLongPressHandled = false; + return true; + } + + mKeyDownReceived = false; + if (mContainer.getTouchState() == LeanbackKeyboardContainer.TOUCH_STATE_CLICK) { + mContainer.setTouchState(LeanbackKeyboardContainer.TOUCH_STATE_TOUCH_SNAP); + mSpaceTracker.unblockMovement(); + } + } + + return handleKeyUpEvent(keyCode, event.getEventTime()); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardView.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardView.java new file mode 100644 index 0000000..2c7fe9b --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackKeyboardView.java @@ -0,0 +1,639 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +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.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import androidx.core.content.ContextCompat; +import com.liskovsoft.leankeyboard.utils.LeanKeyPreferences; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.Iterator; +import java.util.List; + +public class LeanbackKeyboardView extends FrameLayout { + private static final String TAG = "LbKbView"; + /** + * Space key index (important: wrong value will broke navigation) + */ + public static final int ASCII_PERIOD = 47; + /** + * Keys count among which space key spans (important: wrong value will broke navigation) + */ + public static final int ASCII_PERIOD_LEN = 5; + public static final int ASCII_SPACE = 32; + private static final boolean DEBUG = false; + public static final int KEYCODE_CAPS_LOCK = -6; + public static final int KEYCODE_DELETE = -5; + public static final int KEYCODE_DISMISS_MINI_KEYBOARD = -8; + public static final int KEYCODE_LEFT = -3; + public static final int KEYCODE_RIGHT = -4; + public static final int KEYCODE_SHIFT = -1; + public static final int KEYCODE_SYM_TOGGLE = -2; + public static final int KEYCODE_VOICE = -7; + public static final int KEYCODE_LANG_TOGGLE = -9; + public static final int KEYCODE_CLIPBOARD = -10; + public static final int NOT_A_KEY = -1; + public static final int SHIFT_LOCKED = 2; + public static final int SHIFT_OFF = 0; + public static final int SHIFT_ON = 1; + private int mBaseMiniKbIndex = -1; + private final int mClickAnimDur; + private final float mClickedScale; + private final float mSquareIconScaleFactor; + private int mColCount; + private View mCurrentFocusView; + private boolean mFocusClicked; + private int mFocusIndex; + private final float mFocusedScale; + private final int mInactiveMiniKbAlpha; + private ImageView[] mKeyImageViews; + private int mKeyTextColor; + private Keyboard mKeyboard; + private KeyHolder[] mKeys; + private boolean mMiniKeyboardOnScreen; + private Rect mPadding; + private int mRowCount; + private int mShiftState; + private final int mUnfocusStartDelay; + private final KeyConverter mConverter; + protected Paint mPaint; + protected int mKeyTextSize; + protected int mModeChangeTextSize; + private Drawable mCustomCapsLockDrawable; + + private static class KeyConverter { + private static final int LOWER_CASE = 0; + private static final int UPPER_CASE = 1; + + private void init(KeyHolder keyHolder) { + // store original label + // in case when two characters are stored in one label (e.g. "A|B") + if (keyHolder.key.text == null) { + keyHolder.key.text = keyHolder.key.label; + } + } + + public void toLowerCase(KeyHolder keyHolder) { + extractChar(LOWER_CASE, keyHolder); + } + + public void toUpperCase(KeyHolder keyHolder) { + extractChar(UPPER_CASE, keyHolder); + } + + private void extractChar(int charCase, KeyHolder keyHolder) { + init(keyHolder); + + CharSequence result = null; + CharSequence label = keyHolder.key.text; + + String[] labels = splitLabels(label); + + switch (charCase) { + case LOWER_CASE: + result = labels != null ? labels[0] : label.toString().toLowerCase(); + break; + case UPPER_CASE: + result = labels != null ? labels[1] : label.toString().toUpperCase(); + break; + } + + keyHolder.key.label = result; + } + + private String[] splitLabels(CharSequence label) { + String realLabel = label.toString(); + + String[] labels = realLabel.split("\\|"); + + return labels.length == 2 ? labels : null; // remember, we encoding two chars + } + } + + public LeanbackKeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = context.getResources(); + TypedArray styledAttrs = context.getTheme().obtainStyledAttributes(attrs, R.styleable.LeanbackKeyboardView, 0, 0); + mRowCount = styledAttrs.getInteger(R.styleable.LeanbackKeyboardView_rowCount, -1); + mColCount = styledAttrs.getInteger(R.styleable.LeanbackKeyboardView_columnCount, -1); + mKeyTextSize = (int) res.getDimension(R.dimen.key_font_size); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextSize(mKeyTextSize); + mPaint.setTextAlign(Align.CENTER); + mPaint.setAlpha(255); + mPadding = new Rect(0, 0, 0, 0); + mModeChangeTextSize = (int) res.getDimension(R.dimen.function_key_mode_change_font_size); + mKeyTextColor = ContextCompat.getColor(getContext(), R.color.key_text_default); + mFocusIndex = -1; + mShiftState = 0; + mFocusedScale = res.getFraction(R.fraction.focused_scale, 1, 1); + mClickedScale = res.getFraction(R.fraction.clicked_scale, 1, 1); + mSquareIconScaleFactor = res.getFraction(R.fraction.square_icon_scale_factor, 1, 1); + mClickAnimDur = res.getInteger(R.integer.clicked_anim_duration); + mUnfocusStartDelay = res.getInteger(R.integer.unfocused_anim_delay); + mInactiveMiniKbAlpha = res.getInteger(R.integer.inactive_mini_kb_alpha); + mConverter = new KeyConverter(); + } + + private void adjustCase(KeyHolder keyHolder) { + boolean flag = keyHolder.isInMiniKb && keyHolder.isInvertible; + + // ^ equals to != + if (mKeyboard.isShifted() ^ flag) { + mConverter.toUpperCase(keyHolder); + } else { + mConverter.toLowerCase(keyHolder); + } + } + + /** + * NOTE: Adds key views to root window + */ + @SuppressLint("NewApi") + private ImageView createKeyImageView(final int keyIndex) { + Rect padding = mPadding; + int kbdPaddingLeft = getPaddingLeft(); + int kbdPaddingTop = getPaddingTop(); + KeyHolder keyHolder = mKeys[keyIndex]; + Key key = keyHolder.key; + adjustCase(keyHolder); + String label; + if (key.label == null) { + label = null; + } else { + label = key.label.toString(); + } + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "LABEL: " + key.label + "->" + label); + } + + Bitmap bitmap = Bitmap.createBitmap(key.width, key.height, Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = mPaint; + paint.setColor(mKeyTextColor); + canvas.drawARGB(0, 0, 0, 0); + if (key.icon != null) { + if (key.codes[0] == NOT_A_KEY) { + switch (mShiftState) { + case SHIFT_OFF: + key.icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_ime_shift_off); + break; + case SHIFT_ON: + key.icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_ime_shift_on); + break; + case SHIFT_LOCKED: + if (mCustomCapsLockDrawable != null) { + key.icon = mCustomCapsLockDrawable; + } else { + key.icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_ime_shift_lock_on); + } + } + } + + // NOTE: Fix non proper scale of space key on low dpi + + int iconWidth = key.width; // originally used key.icon.getIntrinsicWidth(); + int iconHeight = key.height; // originally used key.icon.getIntrinsicHeight(); + + if (key.width == key.height) { // square key proper fit + int newSize = Math.round(key.width * mSquareIconScaleFactor); + iconWidth = newSize; + iconHeight = newSize; + } + + if (key.codes[0] == ASCII_SPACE && LeanKeyPreferences.instance(getContext()).getEnlargeKeyboard()) { + // space fix for large interface + float gap = getResources().getDimension(R.dimen.keyboard_horizontal_gap); + float gapDelta = (gap * 1.3f) - gap; + iconWidth -= gapDelta * (ASCII_PERIOD_LEN - 1); + } + + int dx = (key.width - padding.left - padding.right - iconWidth) / 2 + padding.left; + int dy = (key.height - padding.top - padding.bottom - iconHeight) / 2 + padding.top; + + canvas.translate((float) dx, (float) dy); + key.icon.setBounds(0, 0, iconWidth, iconHeight); + key.icon.draw(canvas); + canvas.translate((float) (-dx), (float) (-dy)); + } else if (label != null) { + if (label.length() > 1) { + paint.setTextSize((float) mModeChangeTextSize); + paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL)); + } else { + paint.setTextSize((float) mKeyTextSize); + paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + } + + canvas.drawText( + label, + (float) ((key.width - padding.left - padding.right) / 2 + padding.left), + (float) ((key.height - padding.top - padding.bottom) / 2) + (paint.getTextSize() - paint.descent()) / 2.0F + (float) padding.top, + paint + ); + paint.setShadowLayer(0.0F, 0.0F, 0.0F, 0); + } + + ImageView image = new ImageView(getContext()); + image.setImageBitmap(bitmap); + image.setContentDescription(label); + // Adds key views to root window + addView(image, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + // Set position manually for each key + image.setX((float) (key.x + kbdPaddingLeft)); + image.setY((float) (key.y + kbdPaddingTop)); + int opacity; + if (mMiniKeyboardOnScreen && !keyHolder.isInMiniKb) { + opacity = mInactiveMiniKbAlpha; + } else { + opacity = 255; + } + + image.setImageAlpha(opacity); + image.setVisibility(View.VISIBLE); + + return image; + } + + private void createKeyImageViews(KeyHolder[] keys) { + if (mKeyImageViews != null) { + ImageView[] images = mKeyImageViews; + int totalImages = images.length; + + for (int i = 0; i < totalImages; ++i) { + removeView(images[i]); + } + + mKeyImageViews = null; + } + + int totalKeys = keys.length; + for (int i = 0; i < totalKeys; ++i) { + if (mKeyImageViews == null) { + mKeyImageViews = new ImageView[totalKeys]; + } else if (mKeyImageViews[i] != null) { + removeView(mKeyImageViews[i]); + } + + mKeyImageViews[i] = createKeyImageView(i); + } + + } + + private void removeMessages() { + // TODO: not implemented + Log.w(TAG, "method 'removeMessages()' not implemented"); + } + + /** + * NOTE: Keys initialization routine.
+ * Any manipulations with keys should be done here. + */ + private void setKeys(List keys) { + mKeys = new KeyHolder[keys.size()]; + Iterator iterator = keys.iterator(); + + for (int i = 0; i < mKeys.length && iterator.hasNext(); ++i) { + Key key = iterator.next(); + mKeys[i] = new KeyHolder(key); + } + } + + public boolean dismissMiniKeyboard() { + boolean dismiss = false; + if (mMiniKeyboardOnScreen) { + mMiniKeyboardOnScreen = false; + setKeys(mKeyboard.getKeys()); + invalidateAllKeys(); + dismiss = true; + } + + return dismiss; + } + + public int getBaseMiniKbIndex() { + return mBaseMiniKbIndex; + } + + public int getColCount() { + return mColCount; + } + + public Key getFocusedKey() { + return mFocusIndex == -1 ? null : mKeys[mFocusIndex].key; + } + + public Key getKey(int index) { + return mKeys != null && mKeys.length != 0 && index >= 0 && index <= mKeys.length ? mKeys[index].key : null; + } + + public Keyboard getKeyboard() { + return mKeyboard; + } + + /** + * Get index of the key under cursor + *
+ * Resulted index depends on the space key position + * @param x x position + * @param y y position + * @return index of the key + */ + public int getNearestIndex(final float x, final float y) { + int result; + if (mKeys != null && mKeys.length != 0) { + float paddingLeft = (float) getPaddingLeft(); + float paddingTop = (float) getPaddingTop(); + float kbHeight = (float) (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + float kbWidth = (float) (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + final int rows = getRowCount(); + final int cols = getColCount(); + final int indexVert = (int) ((y - paddingTop) / kbHeight * (float) rows); + if (indexVert < 0) { + result = 0; + } else { + result = indexVert; + if (indexVert >= rows) { + result = rows - 1; + } + } + + final int indexHoriz = (int) ((x - paddingLeft) / kbWidth * (float) cols); + int indexFull; + if (indexHoriz < 0) { + indexFull = 0; + } else { + indexFull = indexHoriz; + if (indexHoriz >= cols) { + indexFull = cols - 1; + } + } + + indexFull += mColCount * result; + result = indexFull; + if (indexFull > ASCII_PERIOD) { // key goes beyond space + if (indexFull < (ASCII_PERIOD + ASCII_PERIOD_LEN)) { // key stays within space boundary + result = ASCII_PERIOD; + } + } + + indexFull = result; + if (result >= (ASCII_PERIOD + ASCII_PERIOD_LEN)) { // is key position after space? + indexFull = result - ASCII_PERIOD_LEN + 1; + } + + if (indexFull < 0) { + return 0; + } + + result = indexFull; + if (indexFull >= mKeys.length) { + return mKeys.length - 1; + } + } else { + result = 0; + } + + return result; + } + + public int getRowCount() { + return mRowCount; + } + + public int getShiftState() { + return mShiftState; + } + + public void invalidateAllKeys() { + createKeyImageViews(mKeys); + } + + public void invalidateKey(int keyIndex) { + if (mKeys != null && keyIndex >= 0 && keyIndex < mKeys.length) { + if (mKeyImageViews[keyIndex] != null) { + removeView(mKeyImageViews[keyIndex]); + } + + mKeyImageViews[keyIndex] = createKeyImageView(keyIndex); + } + } + + public boolean isMiniKeyboardOnScreen() { + return mMiniKeyboardOnScreen; + } + + public boolean isShifted() { + return mShiftState == SHIFT_ON || mShiftState == SHIFT_LOCKED; + } + + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + } + + public void onKeyLongPress() { + int popupResId = mKeys[mFocusIndex].key.popupResId; + + if (popupResId != 0) { + dismissMiniKeyboard(); + mMiniKeyboardOnScreen = true; + List accentKeys = (new Keyboard(getContext(), popupResId)).getKeys(); + int totalAccentKeys = accentKeys.size(); + int baseIndex = mFocusIndex; + int currentRow = mFocusIndex / mColCount; + int nextRow = (mFocusIndex + totalAccentKeys) / mColCount; + if (currentRow != nextRow) { + baseIndex = mColCount * nextRow - totalAccentKeys; + } + + mBaseMiniKbIndex = baseIndex; + + for (int i = 0; i < totalAccentKeys; ++i) { + Key accentKey = accentKeys.get(i); + accentKey.x = mKeys[baseIndex + i].key.x; + accentKey.y = mKeys[baseIndex + i].key.y; + accentKey.edgeFlags = mKeys[baseIndex + i].key.edgeFlags; + mKeys[baseIndex + i].key = accentKey; + mKeys[baseIndex + i].isInMiniKb = true; + KeyHolder holder = mKeys[baseIndex + i]; + + holder.isInvertible = i == 0; // uppercase first char + } + + invalidateAllKeys(); + } else { + boolean isSpecialKey = mKeys[mFocusIndex].key.icon != null; // space, paste, voice input etc + + if (!isSpecialKey) { // simply use the same char in uppercase + dismissMiniKeyboard(); + mMiniKeyboardOnScreen = true; + mBaseMiniKbIndex = mFocusIndex; + + mKeys[mFocusIndex].isInMiniKb = true; + mKeys[mFocusIndex].isInvertible = true; + + invalidateAllKeys(); + } + } + } + + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mKeyboard == null) { + setMeasuredDimension(getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); + } else { + int heightFull = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); + heightMeasureSpec = heightFull; + if (MeasureSpec.getSize(widthMeasureSpec) < heightFull + 10) { + heightMeasureSpec = MeasureSpec.getSize(widthMeasureSpec); + } + + setMeasuredDimension(heightMeasureSpec, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); + } + } + + public void setFocus(int row, int col, boolean clicked) { + setFocus(mColCount * row + col, clicked); + } + + public void setFocus(int index, boolean clicked) { + setFocus(index, clicked, true); + } + + /** + * NOTE: Increase size of currently focused or clicked key + * @param index index of the key + * @param clicked key state + * @param showFocusScale increase size + */ + public void setFocus(final int index, final boolean clicked, final boolean showFocusScale) { + float scale = 1.0F; + if (mKeyImageViews != null && mKeyImageViews.length != 0) { + int indexFull; + + if (index >= 0 && index < mKeyImageViews.length) { + indexFull = index; + } else { + indexFull = -1; + } + + if (indexFull != mFocusIndex || clicked != mFocusClicked) { + if (indexFull != mFocusIndex) { + if (mFocusIndex != -1) { + LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[mFocusIndex], false); + } + + if (indexFull != -1) { + LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[indexFull], true); + } + } + + if (mCurrentFocusView != null) { + mCurrentFocusView.animate() + .scaleX(scale) + .scaleY(scale) + .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) + .setStartDelay(mUnfocusStartDelay); + + mCurrentFocusView.animate() + .setDuration(mClickAnimDur) + .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) + .setStartDelay(mUnfocusStartDelay); + } + + if (indexFull != -1) { + if (clicked) { + scale = mClickedScale; + } else if (showFocusScale) { + scale = mFocusedScale; + } + + mCurrentFocusView = mKeyImageViews[indexFull]; + mCurrentFocusView.animate() + .scaleX(scale) + .scaleY(scale) + .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) + .setDuration(mClickAnimDur) + .start(); + } + + mFocusIndex = indexFull; + mFocusClicked = clicked; + if (-1 != indexFull && !mKeys[indexFull].isInMiniKb) { + dismissMiniKeyboard(); + } + } + } + + } + + public void setKeyboard(Keyboard keyboard) { + removeMessages(); + mKeyboard = keyboard; + setKeys(mKeyboard.getKeys()); + int state = mShiftState; + mShiftState = -1; + setShiftState(state); + requestLayout(); + invalidateAllKeys(); + } + + /** + * Set keyboard shift sate + * @param state one of the + * {@link LeanbackKeyboardView#SHIFT_ON SHIFT_ON}, + * {@link LeanbackKeyboardView#SHIFT_OFF SHIFT_OFF}, + * {@link LeanbackKeyboardView#SHIFT_LOCKED SHIFT_LOCKED} + * constants + */ + public void setShiftState(int state) { + if (mShiftState != state) { + switch (state) { + case SHIFT_OFF: + mKeyboard.setShifted(false); + break; + case SHIFT_ON: + case SHIFT_LOCKED: + mKeyboard.setShifted(true); + } + + mShiftState = state; + invalidateAllKeys(); + } + } + + private static class KeyHolder { + public boolean isInMiniKb = false; + public boolean isInvertible = false; + public Key key; + + public KeyHolder(Key key) { + this.key = key; + } + } + + public void setCapsLockDrawable(Drawable drawable) { + mCustomCapsLockDrawable = drawable; + } + + public void setKeyTextColor(int color) { + mKeyTextColor = color; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackLocales.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackLocales.java new file mode 100644 index 0000000..f9e1377 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackLocales.java @@ -0,0 +1,92 @@ +package com.liskovsoft.leankeyboard.ime; + +import java.util.Locale; + +public class LeanbackLocales { + public static final Locale ALBANIANIAN; + public static final Locale AZERBAIJANI; + public static final Locale[] AZERTY; + public static final Locale BASQUE_SPANISH; + public static final Locale BELGIAN_DUTCH; + public static final Locale BRITISH_ENGLISH = new Locale("en", "GB"); + public static final Locale CANADIAN_FRENCH; + public static final Locale CATALAN; + public static final Locale CROATIAN; + public static final Locale CZECH; + public static final Locale DANISH; + public static final Locale ENGLISH; + public static final Locale ESTONIAN; + public static final Locale FINNISH; + public static final Locale FRENCH; + public static final Locale GALIC_SPANISH; + public static final Locale GERMAN; + public static final Locale HUNGARIAN; + public static final Locale INDIAN_ENGLISH; + public static final Locale NORWEGIAN; + public static final Locale OTHER_SPANISH; + public static final Locale[] QWERTY_AZ; + public static final Locale[] QWERTY_CA; + public static final Locale[] QWERTY_DA; + public static final Locale[] QWERTY_ES_EU; + public static final Locale[] QWERTY_ES_US; + public static final Locale[] QWERTY_ET; + public static final Locale[] QWERTY_FI; + public static final Locale[] QWERTY_GB; + public static final Locale[] QWERTY_IN; + public static final Locale[] QWERTY_NB; + public static final Locale[] QWERTY_SV; + public static final Locale[] QWERTY_US; + public static final Locale[] QWERTZ; + public static final Locale[] QWERTZ_CH; + public static final Locale SERBIAN; + public static final Locale SLOVENIAN; + public static final Locale SPAIN_SPANISH; + public static final Locale SWEDISH; + public static final Locale SWISS_FRENCH; + public static final Locale SWISS_GERMAN; + public static final Locale SWISS_ITALIAN; + + static { + QWERTY_GB = new Locale[]{BRITISH_ENGLISH}; + INDIAN_ENGLISH = new Locale("en", "IN"); + QWERTY_IN = new Locale[]{INDIAN_ENGLISH}; + SPAIN_SPANISH = new Locale("es", "ES"); + GALIC_SPANISH = new Locale("gl", "ES"); + BASQUE_SPANISH = new Locale("eu", "ES"); + QWERTY_ES_EU = new Locale[]{SPAIN_SPANISH, GALIC_SPANISH, BASQUE_SPANISH}; + OTHER_SPANISH = new Locale("es", ""); + QWERTY_ES_US = new Locale[]{OTHER_SPANISH}; + AZERBAIJANI = new Locale("az", ""); + QWERTY_AZ = new Locale[]{AZERBAIJANI}; + CATALAN = new Locale("ca", ""); + QWERTY_CA = new Locale[]{CATALAN}; + DANISH = new Locale("da", ""); + QWERTY_DA = new Locale[]{DANISH}; + ESTONIAN = new Locale("et", ""); + QWERTY_ET = new Locale[]{ESTONIAN}; + FINNISH = new Locale("fi", ""); + QWERTY_FI = new Locale[]{FINNISH}; + NORWEGIAN = new Locale("nb", ""); + QWERTY_NB = new Locale[]{NORWEGIAN}; + SWEDISH = new Locale("sv", ""); + QWERTY_SV = new Locale[]{SWEDISH}; + ENGLISH = Locale.ENGLISH; + CANADIAN_FRENCH = Locale.CANADA_FRENCH; + QWERTY_US = new Locale[]{ENGLISH, CANADIAN_FRENCH}; + SWISS_GERMAN = new Locale("de", "CH"); + SWISS_ITALIAN = new Locale("it", "CH"); + QWERTZ_CH = new Locale[]{SWISS_GERMAN, SWISS_ITALIAN}; + GERMAN = new Locale("de", ""); + CROATIAN = new Locale("hr", ""); + CZECH = new Locale("cs", ""); + SWISS_FRENCH = new Locale("fr", "CH"); + HUNGARIAN = new Locale("hu", ""); + SERBIAN = new Locale("sr", ""); + SLOVENIAN = new Locale("sl", ""); + ALBANIANIAN = new Locale("sq", ""); + QWERTZ = new Locale[]{GERMAN, CROATIAN, CZECH, SWISS_FRENCH, SWISS_ITALIAN, HUNGARIAN, SERBIAN, SLOVENIAN, ALBANIANIAN}; + FRENCH = Locale.FRENCH; + BELGIAN_DUTCH = new Locale("nl", "BE"); + AZERTY = new Locale[]{FRENCH, BELGIAN_DUTCH}; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackSuggestionsFactory.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackSuggestionsFactory.java new file mode 100644 index 0000000..44582f0 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackSuggestionsFactory.java @@ -0,0 +1,96 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.inputmethodservice.InputMethodService; +import android.text.InputType; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; +import com.liskovsoft.leankeykeyboard.R; + +import java.util.ArrayList; + +public class LeanbackSuggestionsFactory { + private static final String TAG = "LbSuggestionsFactory"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); // Use short text tag to fix "Log tag exceeds limit of 23 characters" + private static final int MODE_AUTO_COMPLETE = 2; + private static final int MODE_DEFAULT = 0; + private static final int MODE_DOMAIN = 1; + private InputMethodService mContext; + private int mMode; + private int mNumSuggestions; + private final ArrayList mSuggestions = new ArrayList<>(); + + public LeanbackSuggestionsFactory(InputMethodService context, int numSuggestions) { + mContext = context; + mNumSuggestions = numSuggestions; + } + + public void clearSuggestions() { + mSuggestions.clear(); + mSuggestions.add(null); // make room for user input, see LeanbackKeyboardContainer.addUserInputToSuggestions + } + + public void createSuggestions() { + clearSuggestions(); + if (mMode == MODE_DOMAIN) { + String[] domains = mContext.getResources().getStringArray(R.array.common_domains); + int totalDomains = domains.length; + + for (int i = 0; i < totalDomains; ++i) { + String domain = domains[i]; + mSuggestions.add(domain); + } + } + + } + + public ArrayList getSuggestions() { + return mSuggestions; + } + + public void onDisplayCompletions(CompletionInfo[] infos) { + createSuggestions(); + int len; + if (infos == null) { + len = 0; + } else { + len = infos.length; + } + + for (int i = 0; i < len && mSuggestions.size() < mNumSuggestions && !TextUtils.isEmpty(infos[i].getText()); ++i) { + mSuggestions.add(i, infos[i].getText().toString()); + } + + if (DEBUG) { + for (len = 0; len < mSuggestions.size(); ++len) { + Log.d(TAG, "completion " + len + ": " + mSuggestions.get(len)); + } + } + + } + + public void onStartInput(EditorInfo info) { + mMode = MODE_DEFAULT; + if ((info.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) { + mMode = MODE_AUTO_COMPLETE; + } + + switch (LeanbackUtils.getInputTypeClass(info)) { + case InputType.TYPE_CLASS_TEXT: + switch (LeanbackUtils.getInputTypeVariation(info)) { + case InputType.TYPE_DATETIME_VARIATION_TIME: + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + mMode = MODE_DOMAIN; + return; + default: + return; + } + default: + } + } + + public boolean shouldSuggestionsAmend() { + return mMode == MODE_DOMAIN; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackUtils.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackUtils.java new file mode 100644 index 0000000..cbd923c --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/LeanbackUtils.java @@ -0,0 +1,198 @@ +package com.liskovsoft.leankeyboard.ime; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import android.text.InputType; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; +import androidx.core.text.BidiFormatter; +import com.liskovsoft.leankeyboard.ime.LeanbackKeyboardContainer.KeyFocus; +import com.liskovsoft.leankeykeyboard.R; + +public class LeanbackUtils { + private static final int ACCESSIBILITY_DELAY_MS = 250; + private static final String EDITOR_LABEL = "label"; + private static final Handler sAccessibilityHandler = new Handler(); + private static final String TAG = LeanbackUtils.class.getSimpleName(); + + public static int getImeAction(EditorInfo info) { + return info.imeOptions & (EditorInfo.IME_FLAG_NO_ENTER_ACTION | EditorInfo.IME_MASK_ACTION); + } + + /** + * Get class of the input + * @param info attrs + * @return constant e.g. {@link InputType#TYPE_CLASS_TEXT InputType.TYPE_CLASS_TEXT} + */ + public static int getInputTypeClass(EditorInfo info) { + return info.inputType & InputType.TYPE_MASK_CLASS; + } + + /** + * Get variation of the input + * @param info attrs + * @return constant e.g. {@link InputType#TYPE_DATETIME_VARIATION_DATE InputType.TYPE_DATETIME_VARIATION_DATE} + */ + public static int getInputTypeVariation(EditorInfo info) { + return info.inputType & InputType.TYPE_MASK_VARIATION; + } + + public static boolean isAlphabet(int letter) { + return Character.isLetter(letter); + } + + @SuppressLint("NewApi") + public static void sendAccessibilityEvent(final View view, boolean focusGained) { + if (view != null && focusGained) { + sAccessibilityHandler.removeCallbacksAndMessages(null); + sAccessibilityHandler.postDelayed(() -> view.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT), ACCESSIBILITY_DELAY_MS); + } + + } + + public static int getAmpersandLocation(InputConnection connection) { + String text = getEditorText(connection); + int pos = text.indexOf(64); + if (pos < 0) { // not found + pos = text.length(); + } + + return pos; + } + + public static int getCharLengthAfterCursor(InputConnection connection) { + int len = 0; + CharSequence after = connection.getTextAfterCursor(1000, 0); + if (after != null) { + len = after.length(); + } + + return len; + } + + public static int getCharLengthBeforeCursor(InputConnection connection) { + int len = 0; + CharSequence before = connection.getTextBeforeCursor(1000, 0); + if (before != null) { + len = before.length(); + } + + return len; + } + + public static String getEditorText(InputConnection connection) { + StringBuilder result = new StringBuilder(); + CharSequence before = connection.getTextBeforeCursor(1000, 0); + CharSequence after = connection.getTextAfterCursor(1000, 0); + if (before != null) { + result.append(before); + } + + if (after != null) { + result.append(after); + } + + return result.toString(); + } + + public static void sendEnterKey(InputConnection connection) { + connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); + } + + public static String getEditorLabel(EditorInfo info) { + if (info != null && info.extras != null && info.extras.containsKey(EDITOR_LABEL)) { + return info.extras.getString(EDITOR_LABEL); + } + + return null; + } + + public static DisplayMetrics createMetricsFrom(Context context, float factor) { + DisplayMetrics metrics = null; + Object service = context.getSystemService(Context.WINDOW_SERVICE); + + if (service instanceof WindowManager) { + WindowManager manager = (WindowManager) service; + metrics = new DisplayMetrics(); + manager.getDefaultDisplay().getMetrics(metrics); + Log.d(TAG, metrics.toString()); + + // new values + metrics.density *= factor; + metrics.densityDpi *= factor; + metrics.heightPixels *= factor; + metrics.widthPixels *= factor; + metrics.scaledDensity *= factor; + metrics.xdpi *= factor; + metrics.ydpi *= factor; + } + + return metrics; + } + + public static void showKeyboardPicker(Context context) { + if (context != null) { + InputMethodManager imeManager = (InputMethodManager) context.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imeManager != null) { + imeManager.showInputMethodPicker(); + } + } + } + + public static int getRtlLenAfterCursor(CharSequence text) { + if (text == null || text.length() == 0) { + return 0; + } + + BidiFormatter formatter = BidiFormatter.getInstance(); + int len = 0; + + for (int i = 1; i < text.length(); i++) { + CharSequence charSequence = text.subSequence(len, i); + if (formatter.isRtl(charSequence)) { + len++; + } else { + break; + } + } + + return len; + } + + public static int getRtlLenBeforeCursor(CharSequence text) { + if (text == null || text.length() == 0) { + return 0; + } + + BidiFormatter formatter = BidiFormatter.getInstance(); + int len = 0; + + for (int i = text.length(); i > 0; i--) { + CharSequence charSequence = text.subSequence(i-1, i); + if (formatter.isRtl(charSequence)) { + len++; + } else { + break; + } + } + + return len; + } + + public static boolean isSubmitButton(KeyFocus focus) { + return focus.index == 0 && focus.type == KeyFocus.TYPE_ACTION; + } + + public static boolean isSuggestionsButton(KeyFocus focus) { + return focus.type == KeyFocus.TYPE_SUGGESTION; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/pano/util/TouchNavMotionTracker.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/pano/util/TouchNavMotionTracker.java new file mode 100644 index 0000000..b0ad731 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/pano/util/TouchNavMotionTracker.java @@ -0,0 +1,164 @@ +package com.liskovsoft.leankeyboard.ime.pano.util; + +import android.annotation.SuppressLint; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.InputDevice.MotionRange; + +public class TouchNavMotionTracker { + private static final float MAXIMUM_FLING_VELOCITY = 1270.0F; + private static final float MINIMUM_FLING_VELOCITY = 200.0F; + private float mCurrX; + private float mCurrY; + private MotionEvent mDownEvent; + private final float mMaxFlingVelocityX; + private final float mMaxFlingVelocityY; + private final float mMinFlingVelocityX; + private final float mMinFlingVelocityY; + private final float mMinScrollX; + private final float mMinScrollY; + private float mPrevX; + private float mPrevY; + private final float mResolutionX; + private final float mResolutionY; + private float mScrollX; + private float mScrollY; + private float mVelX; + private float mVelY; + private VelocityTracker mVelocityTracker; + + public TouchNavMotionTracker(float resolutionX, float resolutionY, float minScrollDist) { + if (resolutionX <= 0.0F) { + resolutionX = 6.3F; + } + + mResolutionX = resolutionX; + if (resolutionY <= 0.0F) { + resolutionY = 6.3F; + } + + mResolutionY = resolutionY; + mMaxFlingVelocityX = mResolutionX * MAXIMUM_FLING_VELOCITY; + mMaxFlingVelocityY = mResolutionY * MAXIMUM_FLING_VELOCITY; + mMinFlingVelocityX = mResolutionX * MINIMUM_FLING_VELOCITY; + mMinFlingVelocityY = mResolutionY * MINIMUM_FLING_VELOCITY; + mMinScrollX = mResolutionX * minScrollDist; + mMinScrollY = mResolutionY * minScrollDist; + } + + @SuppressLint("NewApi") + public static TouchNavMotionTracker buildTrackerForDevice(final InputDevice device, final float minScrollDist) { + MotionRange range = device.getMotionRange(0); + float resolution; + if (range == null) { + resolution = 0.0F; + } else { + resolution = range.getResolution(); + } + + float resolutionX = resolution; + if (resolution <= 0.0F) { + resolutionX = 6.3F; + } + + MotionRange range2 = device.getMotionRange(1); + if (range2 == null) { + resolution = 0.0F; + } else { + resolution = range2.getResolution(); + } + + float resolutionY = resolution; + if (resolution <= 0.0F) { + resolutionY = 6.3F; + } + + return new TouchNavMotionTracker(resolutionX, resolutionY, minScrollDist); + } + + public void addMovement(MotionEvent event) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + + mVelocityTracker.addMovement(event); + } + + public void clear() { + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + } + + public boolean computeVelocity() { + mVelocityTracker.computeCurrentVelocity(1000); + mVelX = Math.min(mMaxFlingVelocityX, mVelocityTracker.getXVelocity()); + mVelY = Math.min(mMaxFlingVelocityY, mVelocityTracker.getYVelocity()); + return Math.abs(mVelX) > mMinFlingVelocityX || Math.abs(mVelY) > mMinFlingVelocityY; + } + + public MotionEvent getDownEvent() { + return mDownEvent; + } + + public float getPhysicalX(float x) { + return x / mResolutionX; + } + + public float getPhysicalY(float y) { + return y / mResolutionY; + } + + public float getScrollX() { + return mScrollX; + } + + public float getScrollY() { + return mScrollY; + } + + public float getXResolution() { + return mResolutionX; + } + + public float getXVel() { + return mVelX; + } + + public float getYResolution() { + return mResolutionY; + } + + public float getYVel() { + return mVelY; + } + + public void setDownEvent(MotionEvent event) { + if (mDownEvent != null && event != mDownEvent) { + mDownEvent.recycle(); + } + + mDownEvent = event; + } + + public boolean setNewValues(float currX, float currY) { + mCurrX = currX; + mCurrY = currY; + mScrollX = mCurrX - mPrevX; + mScrollY = mCurrY - mPrevY; + return Math.abs(mScrollX) > mMinScrollX || Math.abs(mScrollY) > mMinScrollY; + } + + public void updatePrevValues() { + mPrevX = mCurrX; + mPrevY = mCurrY; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/pano/util/TouchNavSpaceTracker.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/pano/util/TouchNavSpaceTracker.java new file mode 100644 index 0000000..6ef0f9a --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/pano/util/TouchNavSpaceTracker.java @@ -0,0 +1,599 @@ +package com.liskovsoft.leankeyboard.ime.pano.util; + +import android.animation.TimeInterpolator; +import android.graphics.PointF; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.util.SparseArray; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.animation.AccelerateInterpolator; + +public class TouchNavSpaceTracker { + private static final boolean DEBUG = false; + public static final float DEFAULT_DAMPED_SENSITIVITY = 0.5F; + public static final long DEFAULT_DAMPENING_DURATION_MS = 200L; + public static final long DEFAULT_DAMPING_DURATION_MS = 200L; + public static final float DEFAULT_HORIZONTAL_SIZE_MM = 120.0F; + public static final float DEFAULT_LPF_COEFF = 0.25F; + public static final float DEFAULT_MAX_FLICK_DISTANCE_MM = 40.0F; + public static final long DEFAULT_MAX_FLICK_DURATION_MS = 250L; + public static final float DEFAULT_MIN_FLICK_DISTANCE_MM = 4.0F; + public static final float DEFAULT_SENSITIVITY = 1.0F; + public static final float DEFAULT_VERTICAL_SIZE_MM = 50.0F; + private static final int[] DIRECTIONS = new int[]{1, 3, 2, 6, 4, 12, 8, 9, 1}; + private static final float[] DIRECTION_BOUNDARIES = new float[]{-2.7488935F, -1.9634954F, -1.1780972F, -0.3926991F, 0.3926991F, 1.1780972F, + 1.9634954F, 2.7488935F}; + public static final int DIRECTION_DOWN = 2; + public static final int DIRECTION_DOWN_LEFT = 3; + public static final int DIRECTION_DOWN_RIGHT = 6; + public static final int DIRECTION_LEFT = 1; + public static final int DIRECTION_RIGHT = 4; + public static final int DIRECTION_UP = 8; + public static final int DIRECTION_UP_LEFT = 9; + public static final int DIRECTION_UP_RIGHT = 12; + private static final int MSG_LONG_CLICK = 0; + private static final String TAG = "TouchNavSpaceTracker"; + private float mDampedSensitivity; + private float mDampingDuration; + private float mFlickMaxDistance; + private long mFlickMaxDuration; + private float mFlickMaxSquared; + private float mFlickMinDistance; + private float mFlickMinSquared; + private Handler mHandler; + protected TouchNavSpaceTracker.KeyEventListener mKeyEventListener; + private float mLPFCurrX; + private float mLPFCurrY; + private boolean mLPFEnabled; + private long mMovementBlockTime; + private float mPhysicalHeight; + private PointF mPhysicalPosition; + private float mPhysicalWidth; + private float mPixelHeight; + protected TouchNavSpaceTracker.TouchEventListener mPixelListener; + private float mPixelWidth; + private float mPixelsPerMm; + private PointF mPrevPhysPosition; + private float mSensitivity; + private TimeInterpolator mSensitivityInterpolator; + protected final SparseArray mTouchParams; + private float mUnscaledFlickMaxDistance; + private float mUnscaledFlickMinDistance; + private boolean mWasBlocked; + + public TouchNavSpaceTracker() { + this(null, null); + } + + public TouchNavSpaceTracker(TouchNavSpaceTracker.KeyEventListener keyListener, TouchNavSpaceTracker.TouchEventListener pixelSpaceListener) { + mPrevPhysPosition = new PointF(Float.MIN_VALUE, Float.MIN_VALUE); + mPhysicalPosition = new PointF(Float.MIN_VALUE, Float.MIN_VALUE); + mWasBlocked = false; + mSensitivityInterpolator = new AccelerateInterpolator(); + mDampingDuration = DEFAULT_DAMPING_DURATION_MS; + mDampedSensitivity = DEFAULT_DAMPED_SENSITIVITY; + mSensitivity = DEFAULT_SENSITIVITY; + mUnscaledFlickMinDistance = DEFAULT_MIN_FLICK_DISTANCE_MM; + mUnscaledFlickMaxDistance = DEFAULT_MAX_FLICK_DISTANCE_MM; + mFlickMinDistance = mSensitivity * DEFAULT_MIN_FLICK_DISTANCE_MM; + mFlickMaxDistance = mSensitivity * DEFAULT_MAX_FLICK_DISTANCE_MM; + mFlickMinSquared = mFlickMinDistance * mFlickMinDistance; + mFlickMaxSquared = mFlickMaxDistance * mFlickMaxDistance; + mFlickMaxDuration = DEFAULT_MAX_FLICK_DURATION_MS; + mLPFEnabled = false; + mHandler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case 0: + if (TouchNavSpaceTracker.this.mKeyEventListener != null) { + TouchNavSpaceTracker.this.mKeyEventListener.onKeyLongPress(msg.arg1, (KeyEvent) msg.obj); + return; + } + default: + } + } + }; + mKeyEventListener = keyListener; + mPixelListener = pixelSpaceListener; + mTouchParams = new SparseArray<>(1); + mPhysicalWidth = DEFAULT_HORIZONTAL_SIZE_MM; + mPhysicalHeight = DEFAULT_VERTICAL_SIZE_MM; + mPixelWidth = 0.0F; + mPixelHeight = 0.0F; + mPixelsPerMm = 0.0F; + } + + private float calculateSensitivity(MotionEvent var1, MotionEvent var2) { + long var4 = var1.getEventTime() - var2.getEventTime(); + float var3; + if (var1.getEventTime() < this.mMovementBlockTime) { + var3 = 0.0F; + this.mWasBlocked = true; + } else if ((float) var4 < this.mDampingDuration) { + var3 = this.mSensitivityInterpolator.getInterpolation((float) var4 / this.mDampingDuration); + var3 = this.mDampedSensitivity + (this.mSensitivity - this.mDampedSensitivity) * var3; + } else { + var3 = this.mSensitivity; + } + + if (var3 != 0.0F && this.mWasBlocked) { + this.mWasBlocked = false; + this.setPhysicalPosition(this.mPhysicalPosition.x, this.mPhysicalPosition.y); + } + + return var3; + } + + private void checkForLongClick(int var1, KeyEvent event) { + if (var1 == 23) { + Message msg = this.mHandler.obtainMessage(0); + msg.arg1 = var1; + msg.obj = event; + if (!this.mHandler.hasMessages(0)) { + this.mHandler.sendMessageDelayed(msg, (long) ViewConfiguration.getLongPressTimeout()); + return; + } + } + + } + + private void clampPosition() { + if (mPhysicalPosition.x < 0.0F) { + setPhysicalPosition(0.0F, mPhysicalPosition.y); + } else if (mPhysicalPosition.x > mPhysicalWidth) { + setPhysicalPosition(mPhysicalWidth, mPhysicalPosition.y); + } + + if (mPhysicalPosition.y < 0.0F) { + setPhysicalPosition(mPhysicalPosition.x, 0.0F); + } else if (mPhysicalPosition.y > mPhysicalHeight) { + setPhysicalPosition(mPhysicalPosition.x, mPhysicalHeight); + } + } + + private int getDpadDirection(final float dx, final float dy) { + final float polar = (float) Math.atan2((double) (-dy), (double) dx); + + int idx; + for (idx = 0; idx < DIRECTION_BOUNDARIES.length && polar >= DIRECTION_BOUNDARIES[idx]; ++idx) { + ; + } + + return DIRECTIONS[idx]; + } + + private float getPhysicalX(float x) { + return this.mPixelWidth <= 0.0F ? 0.0F : this.mPhysicalWidth * x / this.mPixelWidth; + } + + private float getPhysicalY(float y) { + return this.mPixelHeight <= 0.0F ? 0.0F : this.mPhysicalHeight * y / this.mPixelHeight; + } + + private float getPixelX(float x) { + return this.mPixelWidth * x / this.mPhysicalWidth; + } + + private float getPixelY(float y) { + return this.mPixelHeight * y / this.mPhysicalHeight; + } + + private int getPrimaryDpadDirection(float var1, float var2) { + if (Math.abs(var1) > Math.abs(var2)) { + return var1 > 0.0F ? 4 : 1; + } else { + return var2 > 0.0F ? 2 : 8; + } + } + + private float getScaledValue(float var1, float var2) { + return var1 * var2; + } + + private TouchNavMotionTracker getTrackerForDevice(InputDevice device) { + TouchNavMotionTracker var3 = (TouchNavMotionTracker) this.mTouchParams.get(device.getId()); + TouchNavMotionTracker var2 = var3; + if (var3 == null) { + var2 = TouchNavMotionTracker.buildTrackerForDevice(device, 0.1F); + this.mTouchParams.put(device.getId(), var2); + } + + return var2; + } + + private void setPhysicalSizeInternal(float var1, float var2) { + this.mPhysicalWidth = var1; + this.mPhysicalHeight = var2; + if (this.mPhysicalPosition.x > this.mPhysicalWidth) { + this.mPhysicalPosition.x = this.mPhysicalWidth; + } + + if (this.mPhysicalPosition.y > this.mPhysicalHeight) { + this.mPhysicalPosition.y = this.mPhysicalHeight; + } + + } + + private void updatePhysicalSize() { + if (this.mPixelWidth > 0.0F && this.mPixelHeight > 0.0F && this.mPixelsPerMm > 0.0F) { + this.setPhysicalSizeInternal(this.mPixelWidth / this.mPixelsPerMm, this.mPixelHeight / this.mPixelsPerMm); + } + + } + + public void blockMovementUntil(long var1) { + this.mMovementBlockTime = var1; + } + + public void configureDamping(float var1, long var2) { + this.mDampedSensitivity = var1; + this.mDampingDuration = (float) var2; + } + + public void configureFlicks(float var1, float var2, long var3) { + this.mUnscaledFlickMinDistance = var1; + this.mUnscaledFlickMaxDistance = var2; + this.mFlickMinDistance = this.mSensitivity * var1; + this.mFlickMaxDistance = this.mSensitivity * var2; + this.mFlickMinSquared = this.mFlickMinDistance * this.mFlickMinDistance; + this.mFlickMaxSquared = this.mFlickMaxDistance * this.mFlickMaxDistance; + this.mFlickMaxDuration = var3; + } + + public PointF getCurrentPhysicalPosition() { + return new PointF(this.mPhysicalPosition.x, this.mPhysicalPosition.y); + } + + public PointF getCurrentPixelPosition() { + return new PointF(this.getPixelX(this.mPhysicalPosition.x), this.getPixelY(this.mPhysicalPosition.y)); + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if (event != null && (event.getSource() & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice.SOURCE_TOUCH_NAVIGATION) { + InputDevice device = event.getDevice(); + if (device == null) { + return false; + } + + TouchNavMotionTracker tracker = this.getTrackerForDevice(device); + int action = event.getActionMasked(); + tracker.addMovement(event); + boolean pointerUp; + if ((action & 255) == MotionEvent.ACTION_POINTER_UP) { + pointerUp = true; + } else { + pointerUp = false; + } + + int skipIndex; + if (pointerUp) { + skipIndex = event.getActionIndex(); + } else { + skipIndex = -1; + } + + float sumX = 0.0F; + float sumY = 0.0F; + int count = event.getPointerCount(); + + for (int i = 0; i < count; ++i) { + if (skipIndex != i) { + sumX += event.getX(i); + sumY += event.getY(i); + } + } + + int div; + if (pointerUp) { + div = count - 1; + } else { + div = count; + } + + float currX = sumX / (float) div; + float currY = sumY / (float) div; + TouchNavSpaceTracker.PhysicalMotionEvent pe = new TouchNavSpaceTracker.PhysicalMotionEvent(event.getDeviceId(), tracker.getPhysicalX + (currX), tracker.getPhysicalX(currY), event.getEventTime()); + boolean var18 = false; + boolean var12 = false; + boolean var11; + MotionEvent var15; + TouchNavSpaceTracker.PhysicalMotionEvent var16; + switch (action & 255) { + case MotionEvent.ACTION_DOWN: + if (mLPFEnabled) { + mLPFCurrX = currX; + mLPFCurrY = currY; + } + + tracker.setNewValues(currX, currY); + tracker.updatePrevValues(); + tracker.setDownEvent(MotionEvent.obtain(event)); + if (mPixelListener != null) { + return mPixelListener.onDown(pe); + } + break; + case MotionEvent.ACTION_UP: + var15 = tracker.getDownEvent(); + if (var15 == null) { + Log.w("TouchNavSpaceTracker", "Up event without down event"); + return false | this.mPixelListener.onUp(pe, this.getPixelX(this.mPhysicalPosition.x), + this.getPixelY(this.mPhysicalPosition.y)); + } + + var16 = new TouchNavSpaceTracker.PhysicalMotionEvent(event.getDeviceId(), tracker.getPhysicalX(var15.getX()), tracker.getPhysicalY + (var15.getY()), var15.getEventTime()); + pointerUp = var18; + if (tracker.computeVelocity()) { + pointerUp = var18; + if (this.mPixelListener != null) { + sumY = this.getPixelX(tracker.getPhysicalX(tracker.getXVel())); + sumX = this.getPixelY(tracker.getPhysicalY(tracker.getYVel())); + var18 = false | this.mPixelListener.onFling(var16, pe, sumY, sumX); + pointerUp = var18; + if (pe.getTime() - var16.getTime() < this.mFlickMaxDuration) { + sumY = pe.getX() - var16.getX(); + sumX = pe.getY() - var16.getY(); + currX = sumY * sumY + sumX * sumX; + pointerUp = var18; + if (currX > this.mFlickMinSquared) { + pointerUp = var18; + if (currX < this.mFlickMaxSquared) { + this.mPixelListener.onFlick(var16, pe, this.getDpadDirection(sumY, sumX), this.getPrimaryDpadDirection + (sumY, sumX)); + pointerUp = var18; + } + } + } + } + } + + sumY = this.getPixelX(this.mPhysicalPosition.x); + sumX = this.getPixelY(this.mPhysicalPosition.y); + var11 = this.mPixelListener.onUp(pe, sumY, sumX); + tracker.clear(); + return pointerUp | var11; + case MotionEvent.ACTION_MOVE: + if (tracker.getDownEvent() == null) { + tracker.setDownEvent(MotionEvent.obtain(event)); + if (this.mLPFEnabled) { + this.mLPFCurrX = currX; + this.mLPFCurrY = currY; + } + } + + sumX = currX; + sumY = currY; + if (this.mLPFEnabled) { + this.mLPFCurrX = this.mLPFCurrX * 0.75F + DEFAULT_LPF_COEFF * currX; + this.mLPFCurrY = this.mLPFCurrY * 0.75F + DEFAULT_LPF_COEFF * currY; + sumX = this.mLPFCurrX; + sumY = this.mLPFCurrY; + } + + if (tracker.setNewValues(sumX, sumY)) { + sumY = tracker.getPhysicalX(tracker.getScrollX()); + sumX = tracker.getPhysicalY(tracker.getScrollY()); + currX = this.calculateSensitivity(event, tracker.getDownEvent()); + this.mPhysicalPosition.x = this.mPrevPhysPosition.x + this.getScaledValue(sumY, currX); + this.mPhysicalPosition.y = this.mPrevPhysPosition.y + this.getScaledValue(sumX, currX); + this.clampPosition(); + if (!this.mPhysicalPosition.equals(this.mPrevPhysPosition)) { + var11 = var12; + if (this.mPixelListener != null) { + var11 = var12; + if (this.mPixelHeight > 0.0F) { + var11 = var12; + if (this.mPixelWidth > 0.0F) { + var15 = tracker.getDownEvent(); + var16 = new TouchNavSpaceTracker.PhysicalMotionEvent(event.getDeviceId(), tracker.getPhysicalX(var15.getX()), + tracker.getPhysicalY(var15.getY()), var15.getEventTime()); + sumY = this.getPixelX(this.mPhysicalPosition.x); + sumX = this.getPixelY(this.mPhysicalPosition.y); + var11 = false | this.mPixelListener.onMove(var16, pe, sumY, sumX); + } + } + } + + this.mPrevPhysPosition.set(this.mPhysicalPosition); + } else { + var11 = false | true; + } + + tracker.updatePrevValues(); + return var11; + } + + return false | true; + case MotionEvent.ACTION_CANCEL: + tracker.clear(); + return false; + default: + return false; + } + } + + return false; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (event != null && event.getDevice() != null && (event.getDevice().getSources() & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice + .SOURCE_TOUCH_NAVIGATION) { + if (event.getRepeatCount() == 0) { + checkForLongClick(keyCode, event); + } + + if (mKeyEventListener != null) { + return mKeyEventListener.onKeyDown(keyCode, event); + } + } + + return false; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event != null && event.getDevice() != null && (event.getDevice().getSources() & InputDevice.SOURCE_TOUCH_NAVIGATION) == InputDevice + .SOURCE_TOUCH_NAVIGATION) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { + mHandler.removeMessages(0); + } + + if (mKeyEventListener != null) { + return mKeyEventListener.onKeyUp(keyCode, event); + } + } + + return false; + } + + public void onPause() { + mHandler.removeMessages(0); + } + + public void setKeyEventListener(TouchNavSpaceTracker.KeyEventListener listener) { + mKeyEventListener = listener; + } + + public void setLPFEnabled(boolean enabled) { + mLPFEnabled = enabled; + } + + public void setPhysicalDensity(float density) { + mPixelsPerMm = density; + if (density > 0.0F) { + updatePhysicalSize(); + } + + } + + public void setPhysicalPosition(float x, float y) { + mPhysicalPosition.x = x; + mPhysicalPosition.y = y; + mPrevPhysPosition.x = x; + mPrevPhysPosition.y = y; + clampPosition(); + } + + public void setPhysicalSize(float widthMm, float heightMm) { + if (mPixelsPerMm <= 0.0F) { + setPhysicalSizeInternal(widthMm, heightMm); + } + } + + public void setPixelPosition(float x, float y) { + setPhysicalPosition(getPhysicalX(x), getPhysicalY(y)); + } + + public void setPixelSize(float width, float height) { + mPixelHeight = height; + mPixelWidth = width; + updatePhysicalSize(); + } + + public void setSensitivity(float sensitivity) { + mSensitivity = sensitivity; + configureFlicks(mUnscaledFlickMinDistance, mUnscaledFlickMaxDistance, mFlickMaxDuration); + } + + public void setTouchEventListener(TouchNavSpaceTracker.TouchEventListener listener) { + mPixelListener = listener; + } + + public void unblockMovement() { + this.mMovementBlockTime = 0L; + } + + public interface KeyEventListener { + boolean onKeyDown(int keyCode, KeyEvent event); + + boolean onKeyLongPress(int keyCode, KeyEvent event); + + boolean onKeyUp(int keyCode, KeyEvent event); + } + + public static class PhysicalMotionEvent { + private final int mDeviceId; + private final long mTime; + // $FF: renamed from: mX float + private final float field_6; + // $FF: renamed from: mY float + private final float field_7; + + public PhysicalMotionEvent(int var1, float var2, float var3, long var4) { + this.mDeviceId = var1; + this.field_6 = var2; + this.field_7 = var3; + this.mTime = var4; + } + + public final InputDevice getDevice() { + return InputDevice.getDevice(this.getDeviceId()); + } + + public final int getDeviceId() { + return this.mDeviceId; + } + + public final long getTime() { + return this.mTime; + } + + public final float getX() { + return this.field_6; + } + + public final float getY() { + return this.field_7; + } + } + + public static class SimpleTouchEventListener implements TouchNavSpaceTracker.KeyEventListener, TouchNavSpaceTracker.TouchEventListener { + public boolean onDown(TouchNavSpaceTracker.PhysicalMotionEvent var1) { + return false; + } + + public boolean onFlick(TouchNavSpaceTracker.PhysicalMotionEvent var1, TouchNavSpaceTracker.PhysicalMotionEvent var2, int var3, int var4) { + return false; + } + + public boolean onFling(TouchNavSpaceTracker.PhysicalMotionEvent var1, TouchNavSpaceTracker.PhysicalMotionEvent var2, float var3, float var4) { + return false; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + return false; + } + + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + public boolean onKeyUp(int keyCode, KeyEvent event) { + return false; + } + + public boolean onMove(TouchNavSpaceTracker.PhysicalMotionEvent var1, TouchNavSpaceTracker.PhysicalMotionEvent var2, float var3, float var4) { + return false; + } + + public boolean onUp(TouchNavSpaceTracker.PhysicalMotionEvent var1, float var2, float var3) { + return false; + } + } + + public interface TouchEventListener { + boolean onDown(TouchNavSpaceTracker.PhysicalMotionEvent var1); + + boolean onFlick(TouchNavSpaceTracker.PhysicalMotionEvent var1, TouchNavSpaceTracker.PhysicalMotionEvent var2, int var3, int var4); + + boolean onFling(TouchNavSpaceTracker.PhysicalMotionEvent var1, TouchNavSpaceTracker.PhysicalMotionEvent var2, float var3, float var4); + + boolean onMove(TouchNavSpaceTracker.PhysicalMotionEvent var1, TouchNavSpaceTracker.PhysicalMotionEvent var2, float var3, float var4); + + boolean onUp(TouchNavSpaceTracker.PhysicalMotionEvent var1, float var2, float var3); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/BitmapSoundLevelView.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/BitmapSoundLevelView.java new file mode 100644 index 0000000..49a205a --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/BitmapSoundLevelView.java @@ -0,0 +1,191 @@ +package com.liskovsoft.leankeyboard.ime.voice; + +import android.animation.TimeAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import androidx.core.content.ContextCompat; +import com.liskovsoft.leankeykeyboard.R; + +public class BitmapSoundLevelView extends View { + private static final boolean DEBUG = false; + private static final int MIC_LEVEL_GUIDELINE_OFFSET = 13; + private static final int MIC_PRIMARY_LEVEL_IMAGE_OFFSET = 3; + private static final String TAG = "BitmapSoundLevelsView"; + private TimeAnimator mAnimator; + private final int mCenterTranslationX; + private final int mCenterTranslationY; + private int mCurrentVolume; + private Rect mDestRect; + private final int mDisableBackgroundColor; + private final Paint mEmptyPaint; + private final int mEnableBackgroundColor; + private SpeechLevelSource mLevelSource; + private final int mMinimumLevelSize; + private Paint mPaint; + private int mPeakLevel; + private int mPeakLevelCountDown; + private final Bitmap mPrimaryLevel; + private final Bitmap mTrailLevel; + + public BitmapSoundLevelView(Context context) { + this(context, null); + } + + public BitmapSoundLevelView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + @TargetApi(16) + public BitmapSoundLevelView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mEmptyPaint = new Paint(); + TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.BitmapSoundLevelView, defStyleAttr, 0); + mEnableBackgroundColor = styledAttrs.getColor(R.styleable.BitmapSoundLevelView_enabledBackgroundColor, Color.parseColor("#66FFFFFF")); + mDisableBackgroundColor = styledAttrs.getColor(R.styleable.BitmapSoundLevelView_disabledBackgroundColor, -1); + boolean primaryLevelEnabled = false; + boolean peakLevelEnabled = false; + int primaryLevelId = 0; + if (styledAttrs.hasValue(R.styleable.BitmapSoundLevelView_primaryLevels)) { + primaryLevelId = styledAttrs.getResourceId(R.styleable.BitmapSoundLevelView_primaryLevels, R.drawable.vs_reactive_dark); + primaryLevelEnabled = true; + } + + int trailLevelId = 0; + if (styledAttrs.hasValue(R.styleable.BitmapSoundLevelView_trailLevels)) { + trailLevelId = styledAttrs.getResourceId(R.styleable.BitmapSoundLevelView_trailLevels, R.drawable.vs_reactive_light); + peakLevelEnabled = true; + } + + mCenterTranslationX = styledAttrs.getDimensionPixelOffset(R.styleable.BitmapSoundLevelView_levelsCenterX, 0); + mCenterTranslationY = styledAttrs.getDimensionPixelOffset(R.styleable.BitmapSoundLevelView_levelsCenterY, 0); + mMinimumLevelSize = styledAttrs.getDimensionPixelOffset(R.styleable.BitmapSoundLevelView_minLevelRadius, 0); + styledAttrs.recycle(); + if (primaryLevelEnabled) { + mPrimaryLevel = BitmapFactory.decodeResource(getResources(), primaryLevelId); + } else { + mPrimaryLevel = null; + } + + if (peakLevelEnabled) { + mTrailLevel = BitmapFactory.decodeResource(getResources(), trailLevelId); + } else { + mTrailLevel = null; + } + + mPaint = new Paint(); + mDestRect = new Rect(); + mEmptyPaint.setFilterBitmap(true); + mLevelSource = new SpeechLevelSource(); + mLevelSource.setSpeechLevel(0); + mAnimator = new TimeAnimator(); + mAnimator.setRepeatCount(-1); + mAnimator.setTimeListener((animation, totalTime, deltaTime) -> invalidate()); + } + + @TargetApi(16) + private void startAnimator() { + if (!mAnimator.isStarted()) { + mAnimator.start(); + } + + } + + @TargetApi(16) + private void stopAnimator() { + mAnimator.cancel(); + } + + private void updateAnimatorState() { + if (isEnabled()) { + startAnimator(); + } else { + stopAnimator(); + } + } + + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updateAnimatorState(); + } + + protected void onDetachedFromWindow() { + stopAnimator(); + super.onDetachedFromWindow(); + } + + public void onDraw(Canvas canvas) { + if (isEnabled()) { + canvas.drawColor(mEnableBackgroundColor); + final int level = mLevelSource.getSpeechLevel(); + if (level > mPeakLevel) { + mPeakLevel = level; + mPeakLevelCountDown = 25; + } else if (mPeakLevelCountDown == 0) { + mPeakLevel = Math.max(0, mPeakLevel - 2); + } else { + --mPeakLevelCountDown; + } + + if (level > mCurrentVolume) { + mCurrentVolume += (level - mCurrentVolume) / 4; + } else { + mCurrentVolume = (int) ((float) mCurrentVolume * 0.95F); + } + + final int centerX = mCenterTranslationX + getWidth() / 2; + final int centerY = mCenterTranslationY + getWidth() / 2; + int size; + if (mTrailLevel != null) { + size = (centerX - mMinimumLevelSize) * mPeakLevel / 100 + mMinimumLevelSize; + mDestRect.set(centerX - size, centerY - size, centerX + size, centerY + size); + canvas.drawBitmap(mTrailLevel, null, mDestRect, mEmptyPaint); + } + + if (mPrimaryLevel != null) { + size = (centerX - mMinimumLevelSize) * mCurrentVolume / 100 + mMinimumLevelSize; + mDestRect.set(centerX - size, centerY - size, centerX + size, centerY + size); + canvas.drawBitmap(mPrimaryLevel, null, mDestRect, mEmptyPaint); + mPaint.setColor(ContextCompat.getColor(getContext(), R.color.search_mic_background)); + mPaint.setStyle(Style.FILL); + canvas.drawCircle((float) centerX, (float) centerY, (float) (mMinimumLevelSize - 3), mPaint); + } + + if (mTrailLevel != null && mPrimaryLevel != null) { + mPaint.setColor(ContextCompat.getColor(getContext(), R.color.search_mic_levels_guideline)); + mPaint.setStyle(Style.STROKE); + canvas.drawCircle((float) centerX, (float) centerY, (float) (centerX - 13), mPaint); + } + + } else { + canvas.drawColor(mDisableBackgroundColor); + } + } + + public void onWindowFocusChanged(boolean var1) { + super.onWindowFocusChanged(var1); + if (var1) { + updateAnimatorState(); + } else { + stopAnimator(); + } + } + + public void setEnabled(boolean var1) { + super.setEnabled(var1); + updateAnimatorState(); + } + + public void setLevelSource(SpeechLevelSource var1) { + mLevelSource = var1; + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/RecognizerView.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/RecognizerView.java new file mode 100644 index 0000000..6e5d50c --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/RecognizerView.java @@ -0,0 +1,214 @@ +package com.liskovsoft.leankeyboard.ime.voice; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import com.liskovsoft.leankeyboard.ime.LeanbackUtils; +import com.liskovsoft.leankeykeyboard.R; + +public class RecognizerView extends RelativeLayout { + private static final boolean DEBUG = false; + private static final String TAG = "RecognizerView"; + private RecognizerView.Callback mCallback; + private boolean mEnabled; + protected ImageView mMicButton; + private BitmapSoundLevelView mSoundLevels; + private RecognizerView.State mState; + + private enum State { + LISTENING, MIC_INITIALIZING, NOT_LISTENING, RECOGNIZING, RECORDING; + } + + public RecognizerView(Context context) { + super(context); + } + + public RecognizerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RecognizerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + private void updateState(State state) { + mState = state; + refreshUi(); + } + + public View getMicButton() { + return mMicButton; + } + + public void onAttachedToWindow() { + super.onAttachedToWindow(); + refreshUi(); + } + + public void onClick() { + switch (mState) { + case MIC_INITIALIZING: + default: + return; + case LISTENING: + mCallback.onCancelRecordingClicked(); + return; + case RECORDING: + mCallback.onStopRecordingClicked(); + return; + case RECOGNIZING: + mCallback.onCancelRecordingClicked(); + return; + case NOT_LISTENING: + mCallback.onStartRecordingClicked(); + } + } + + @SuppressLint("MissingSuperCall") + @Override + public void onFinishInflate() { + LayoutInflater.from(this.getContext()).inflate(R.layout.recognizer_view, this, true); + mSoundLevels = (BitmapSoundLevelView) findViewById(R.id.microphone); + mMicButton = (ImageView) findViewById(R.id.recognizer_mic_button); + mState = RecognizerView.State.NOT_LISTENING; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof RecognizerView.SavedState)) { + super.onRestoreInstanceState(state); + } else { + RecognizerView.SavedState savedState = (RecognizerView.SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + mState = savedState.mState; + } + } + + @Override + public Parcelable onSaveInstanceState() { + RecognizerView.SavedState savedState = new RecognizerView.SavedState(super.onSaveInstanceState()); + savedState.mState = mState; + return savedState; + } + + protected void refreshUi() { + if (mEnabled) { + switch (mState) { + case MIC_INITIALIZING: + mMicButton.setImageResource(R.drawable.vs_micbtn_on_selector); + mSoundLevels.setEnabled(false); + return; + case LISTENING: + mMicButton.setImageResource(R.drawable.vs_micbtn_on_selector); + mSoundLevels.setEnabled(true); + return; + case RECORDING: + mMicButton.setImageResource(R.drawable.vs_micbtn_rec_selector); + mSoundLevels.setEnabled(true); + return; + case RECOGNIZING: + mMicButton.setImageResource(R.drawable.vs_micbtn_off_selector); + mSoundLevels.setEnabled(false); + return; + case NOT_LISTENING: + mMicButton.setImageResource(R.drawable.vs_micbtn_off_selector); + mSoundLevels.setEnabled(false); + return; + default: + } + } + } + + public void setCallback(RecognizerView.Callback callback) { + mCallback = callback; + } + + public void setMicEnabled(boolean enabled) { + mEnabled = enabled; + if (enabled) { + mMicButton.setAlpha(1.0F); + mMicButton.setImageResource(R.drawable.ic_voice_available); + } else { + mMicButton.setAlpha(0.1F); + mMicButton.setImageResource(R.drawable.ic_voice_off); + } + } + + public void setMicFocused(boolean focused) { + if (mEnabled) { + if (focused) { + mMicButton.setImageResource(R.drawable.ic_voice_focus); + } else { + mMicButton.setImageResource(R.drawable.ic_voice_available); + } + + LeanbackUtils.sendAccessibilityEvent(mMicButton, focused); + } + + } + + public void setSpeechLevelSource(SpeechLevelSource var1) { + mSoundLevels.setLevelSource(var1); + } + + public void showInitializingMic() { + updateState(State.MIC_INITIALIZING); + } + + public void showListening() { + updateState(State.LISTENING); + } + + public void showNotListening() { + updateState(State.NOT_LISTENING); + } + + public void showRecognizing() { + updateState(State.RECOGNIZING); + } + + public void showRecording() { + updateState(State.RECORDING); + } + + public interface Callback { + void onCancelRecordingClicked(); + + void onStartRecordingClicked(); + + void onStopRecordingClicked(); + } + + public static class SavedState extends BaseSavedState { + public static final Creator CREATOR = new Creator() { + public RecognizerView.SavedState createFromParcel(Parcel var1) { + return new RecognizerView.SavedState(var1); + } + + public RecognizerView.SavedState[] newArray(int var1) { + return new RecognizerView.SavedState[var1]; + } + }; + RecognizerView.State mState; + + private SavedState(Parcel var1) { + super(var1); + this.mState = RecognizerView.State.valueOf(var1.readString()); + } + + public SavedState(Parcelable var1) { + super(var1); + } + + public void writeToParcel(Parcel var1, int var2) { + super.writeToParcel(var1, var2); + var1.writeString(this.mState.toString()); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/SpeechLevelSource.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/SpeechLevelSource.java new file mode 100644 index 0000000..d60cd05 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/ime/voice/SpeechLevelSource.java @@ -0,0 +1,25 @@ +package com.liskovsoft.leankeyboard.ime.voice; + +public class SpeechLevelSource { + private volatile int mSpeechLevel; + + public int getSpeechLevel() { + return mSpeechLevel; + } + + public boolean isValid() { + return mSpeechLevel > 0; + } + + public void reset() { + mSpeechLevel = -1; + } + + public void setSpeechLevel(int speechLevel) { + if (speechLevel >= 0 && speechLevel <= 100) { + mSpeechLevel = speechLevel; + } else { + throw new IllegalArgumentException(); + } + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/receiver/RestartServiceReceiver.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/receiver/RestartServiceReceiver.java new file mode 100644 index 0000000..51cd13b --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/receiver/RestartServiceReceiver.java @@ -0,0 +1,28 @@ +package com.liskovsoft.leankeyboard.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import com.liskovsoft.leankeyboard.ime.LeanbackImeService; + +public class RestartServiceReceiver extends BroadcastReceiver { + private static final String TAG = RestartServiceReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + sendMessageToService(context); + //restartService(context); + } + + private void sendMessageToService(Context context) { + Log.d(TAG, "Sending restart message to the service"); + Intent intent = new Intent(context, LeanbackImeService.class); + intent.putExtra(LeanbackImeService.COMMAND_RESTART, true); + context.startService(intent); + } + + private void restartService(Context context) { + System.exit(0); + } +} \ No newline at end of file diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/receiver/TextUpdateReceiver.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/receiver/TextUpdateReceiver.java new file mode 100644 index 0000000..7fd403f --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/receiver/TextUpdateReceiver.java @@ -0,0 +1,15 @@ +package com.liskovsoft.leankeyboard.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class TextUpdateReceiver extends BroadcastReceiver { + private static final String TAG = TextUpdateReceiver.class.getSimpleName(); + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, intent.toUri(0)); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LangUpdater.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LangUpdater.java new file mode 100644 index 0000000..e7301ca --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LangUpdater.java @@ -0,0 +1,124 @@ +package com.liskovsoft.leankeyboard.utils; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import com.liskovsoft.leankeyboard.helpers.Helpers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +public class LangUpdater { + private static final String LOCALE_EN_US = "en_US"; + private static final String LOCALE_RU = "ru_RU"; + private Context mContext; + private String[] rusPackages = {"dkc.androidtv.tree", "dkc.video.fsbox", "dkc.video.hdbox", "dkc.video.uatv"}; + + public LangUpdater(Context ctx) { + mContext = ctx; + } + + public void update() { + tryToEnableRussian(); + tryToForceEnglishOnDevices(); + tryToRestoreLanguage(); + } + + private void tryToRestoreLanguage() { + String langCode = getPreferredLocale(); + if (langCode != null) { + forceLocale(langCode); + } + } + + private void tryToForceEnglishOnDevices() { + String deviceName = Helpers.getDeviceName(); + switch (deviceName) { + case "ChangHong Android TV (full_mst638)": + forceLocale(LOCALE_EN_US); + } + } + + private void tryToBypassChinese() { + String script = LocaleUtility.getScript(Locale.getDefault()); + if (isChineseScript(script)) { + forceLocale(LOCALE_EN_US); + } + } + + private boolean isChineseScript(String script) { + switch (script) { + case "Hani": + case "Hans": + case "Hant": + return true; + } + return false; + } + + private void tryToEnableRussian() { + List installedPackages = getListInstalledPackages(); + for (String pkgName : installedPackages) { + if (isRussianPackage(pkgName)) { + forceLocale(LOCALE_RU); + } + } + } + + // short lang code. ex: "ru" + private void forceLocale(String langCode) { + if (langCode == null || langCode.isEmpty()) { + return; + } + + Locale locale = parseLangCode(langCode); + LocaleUtility.forceLocaleOld(mContext, locale); + } + + private boolean isRussianPackage(String pkgName) { + for (String rusPackage : rusPackages) { + if (rusPackage.equals(pkgName)){ + return true; + } + } + return false; + } + + private List getListInstalledPackages() { + Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); + mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); + List pkgAppsList = mContext.getPackageManager().queryIntentActivities( mainIntent, 0); + List result = new ArrayList<>(); + for (ResolveInfo info : pkgAppsList) { + result.add(info.activityInfo.packageName); + } + return result; + } + + public String getLocale() { + Configuration config = mContext.getResources().getConfiguration(); + return LocaleUtility.getSystemLocale(config).getLanguage(); + } + + /** + * Get locale as lang code (e.g. zh, ru_RU etc) + * @return lang code + */ + public String getPreferredLocale() { + return LeanKeyPreferences.instance(mContext).getPreferredLanguage(); + } + + public void setPreferredLocale(String langCode) { + LeanKeyPreferences.instance(mContext).setPreferredLanguage(langCode); + } + + private Locale parseLangCode(String langCode) { + StringTokenizer tokenizer = new StringTokenizer(langCode, "_"); + String lang = tokenizer.nextToken(); + String country = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : ""; + return new Locale(lang, country); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LeanKeyPreferences.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LeanKeyPreferences.java new file mode 100644 index 0000000..b2c4aaa --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LeanKeyPreferences.java @@ -0,0 +1,119 @@ +package com.liskovsoft.leankeyboard.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +public final class LeanKeyPreferences { + private static final String APP_RUN_ONCE = "appRunOnce"; + private static final String BOOTSTRAP_SELECTED_LANGUAGE = "bootstrapSelectedLanguage"; + private static final String APP_KEYBOARD_INDEX = "appKeyboardIndex"; + private static final String FORCE_SHOW_KEYBOARD = "forceShowKeyboard"; + private static final String ENLARGE_KEYBOARD = "enlargeKeyboard"; + private static final String KEYBOARD_THEME = "keyboardTheme"; + public static final String THEME_DEFAULT = "Default"; + public static final String THEME_DARK = "Dark"; + public static final String THEME_DARK2 = "Dark2"; + public static final String THEME_DARK3 = "Dark3"; + private static final String SUGGESTIONS_ENABLED = "suggestionsEnabled"; + private static final String CYCLIC_NAVIGATION_ENABLED = "cyclicNavigationEnabled"; + private static final String AUTODETECT_LAYOUT = "autodetectLayout"; + private static LeanKeyPreferences sInstance; + private final Context mContext; + private SharedPreferences mPrefs; + + public static LeanKeyPreferences instance(Context ctx) { + if (sInstance == null) + sInstance = new LeanKeyPreferences(ctx); + return sInstance; + } + + public LeanKeyPreferences(Context context) { + mContext = context.getApplicationContext(); + mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); + } + + public boolean isRunOnce() { + return mPrefs.getBoolean(APP_RUN_ONCE, false); + } + + public void setRunOnce(boolean runOnce) { + mPrefs.edit() + .putBoolean(APP_RUN_ONCE, runOnce) + .apply(); + } + + public void setPreferredLanguage(String name) { + mPrefs.edit() + .putString(BOOTSTRAP_SELECTED_LANGUAGE, name) + .apply(); + } + + public String getPreferredLanguage() { + return mPrefs.getString(BOOTSTRAP_SELECTED_LANGUAGE, ""); + } + + public int getKeyboardIndex() { + return mPrefs.getInt(APP_KEYBOARD_INDEX, 0); + } + + public void setKeyboardIndex(int idx) { + mPrefs.edit() + .putInt(APP_KEYBOARD_INDEX, idx) + .apply(); + } + + public boolean getForceShowKeyboard() { + return mPrefs.getBoolean(FORCE_SHOW_KEYBOARD, true); + } + + public void setForceShowKeyboard(boolean force) { + mPrefs.edit() + .putBoolean(FORCE_SHOW_KEYBOARD, force) + .apply(); + } + + public boolean getEnlargeKeyboard() { + return mPrefs.getBoolean(ENLARGE_KEYBOARD, false); + } + + public void setEnlargeKeyboard(boolean enlarge) { + mPrefs.edit() + .putBoolean(ENLARGE_KEYBOARD, enlarge) + .apply(); + } + + public void setCurrentTheme(String theme) { + mPrefs.edit() + .putString(KEYBOARD_THEME, theme) + .apply(); + } + + public String getCurrentTheme() { + return mPrefs.getString(KEYBOARD_THEME, THEME_DARK3); + } + + public void setSuggestionsEnabled(boolean enabled) { + mPrefs.edit() + .putBoolean(SUGGESTIONS_ENABLED, enabled) + .apply(); + } + + public boolean getSuggestionsEnabled() { + return mPrefs.getBoolean(SUGGESTIONS_ENABLED, true); + } + + public void setCyclicNavigationEnabled(boolean enabled) { + mPrefs.edit() + .putBoolean(CYCLIC_NAVIGATION_ENABLED, enabled) + .apply(); + } + + public boolean isCyclicNavigationEnabled() { + return mPrefs.getBoolean(CYCLIC_NAVIGATION_ENABLED, false); + } + + public boolean getAutodetectLayout() { + return mPrefs.getBoolean(AUTODETECT_LAYOUT, false); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LocaleScript.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LocaleScript.java new file mode 100644 index 0000000..1c9e5e4 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LocaleScript.java @@ -0,0 +1,727 @@ +package com.liskovsoft.leankeyboard.utils; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/* + * Additional info: + * https://en.wikipedia.org/wiki/Writing_system + * https://stackoverflow.com/questions/19153384/how-to-get-the-script-from-a-locale-object-on-android + * https://docs.oracle.com/javase/7/docs/api/java/util/Locale.html#getScript() + * http://unicode.org/iso15924/iso15924-codes.html + * + * Usage: + * String script = LocaleScript.getScript(Locale.getDefault()); + * String script = LocaleScript.getScript(getDefaultLocale(myActivity)); + */ +class LocaleScript { + + public static Map> scriptsByLocale = new HashMap>(); + + public static Map getScriptsMap(String... keyValuePairs) { + Map languages = new HashMap(); + for (int i = 0; i < keyValuePairs.length; i += 2) { + languages.put(keyValuePairs[i], keyValuePairs[i + 1]); + } + return languages; + } + + static { + scriptsByLocale.put("aa", getScriptsMap("", "Latn")); + scriptsByLocale.put("ab", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("abq", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("abr", getScriptsMap("", "")); + scriptsByLocale.put("ace", getScriptsMap("", "Latn")); + scriptsByLocale.put("ach", getScriptsMap("", "Latn")); + scriptsByLocale.put("ada", getScriptsMap("", "Latn")); + scriptsByLocale.put("ady", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ae", getScriptsMap("", "Avst")); + scriptsByLocale.put("af", getScriptsMap("", "Latn")); + scriptsByLocale.put("agq", getScriptsMap("", "Latn")); + scriptsByLocale.put("aii", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ain", getScriptsMap("", "Kana")); + scriptsByLocale.put("ak", getScriptsMap("", "Latn")); + scriptsByLocale.put("akk", getScriptsMap("", "Xsux")); + scriptsByLocale.put("ale", getScriptsMap("", "Latn")); + scriptsByLocale.put("alt", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("am", getScriptsMap("", "Ethi")); + scriptsByLocale.put("amo", getScriptsMap("", "Latn")); + scriptsByLocale.put("an", getScriptsMap("", "Latn")); + scriptsByLocale.put("anp", getScriptsMap("", "Deva")); + scriptsByLocale.put("aoz", getScriptsMap("", "")); + scriptsByLocale.put("ar", getScriptsMap("", "Arab", "IR", "Syrc")); + scriptsByLocale.put("arc", getScriptsMap("", "Armi")); + scriptsByLocale.put("arn", getScriptsMap("", "Latn")); + scriptsByLocale.put("arp", getScriptsMap("", "Latn")); + scriptsByLocale.put("arw", getScriptsMap("", "Latn")); + scriptsByLocale.put("as", getScriptsMap("", "Beng")); + scriptsByLocale.put("asa", getScriptsMap("", "Latn")); + scriptsByLocale.put("ast", getScriptsMap("", "Latn")); + scriptsByLocale.put("atj", getScriptsMap("", "")); + scriptsByLocale.put("av", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("awa", getScriptsMap("", "Deva")); + scriptsByLocale.put("ay", getScriptsMap("", "Latn")); + scriptsByLocale.put("az", getScriptsMap("", "Latn", "AZ", "Cyrl", "IR", "Arab")); + scriptsByLocale.put("ba", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("bal", getScriptsMap("", "Arab", "IR", "Latn", "PK", "Latn")); + scriptsByLocale.put("ban", getScriptsMap("", "Latn", "ID", "Bali")); + scriptsByLocale.put("bap", getScriptsMap("", "")); + scriptsByLocale.put("bas", getScriptsMap("", "Latn")); + scriptsByLocale.put("bax", getScriptsMap("", "Bamu")); + scriptsByLocale.put("bbc", getScriptsMap("", "Latn", "ID", "Batk")); + scriptsByLocale.put("bbj", getScriptsMap("", "")); + scriptsByLocale.put("bci", getScriptsMap("", "")); + scriptsByLocale.put("be", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("bej", getScriptsMap("", "Arab")); + scriptsByLocale.put("bem", getScriptsMap("", "Latn")); + scriptsByLocale.put("bew", getScriptsMap("", "")); + scriptsByLocale.put("bez", getScriptsMap("", "Latn")); + scriptsByLocale.put("bfd", getScriptsMap("", "")); + scriptsByLocale.put("bfq", getScriptsMap("", "Taml")); + scriptsByLocale.put("bft", getScriptsMap("", "Arab")); + scriptsByLocale.put("bfy", getScriptsMap("", "Deva")); + scriptsByLocale.put("bg", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("bgc", getScriptsMap("", "")); + scriptsByLocale.put("bgx", getScriptsMap("", "")); + scriptsByLocale.put("bh", getScriptsMap("", "Deva")); + scriptsByLocale.put("bhb", getScriptsMap("", "Deva")); + scriptsByLocale.put("bhi", getScriptsMap("", "")); + scriptsByLocale.put("bhk", getScriptsMap("", "")); + scriptsByLocale.put("bho", getScriptsMap("", "Deva")); + scriptsByLocale.put("bi", getScriptsMap("", "Latn")); + scriptsByLocale.put("bik", getScriptsMap("", "Latn")); + scriptsByLocale.put("bin", getScriptsMap("", "Latn")); + scriptsByLocale.put("bjj", getScriptsMap("", "Deva")); + scriptsByLocale.put("bjn", getScriptsMap("", "")); + scriptsByLocale.put("bkm", getScriptsMap("", "")); + scriptsByLocale.put("bku", getScriptsMap("", "Latn")); + scriptsByLocale.put("bla", getScriptsMap("", "Latn")); + scriptsByLocale.put("blt", getScriptsMap("", "Tavt")); + scriptsByLocale.put("bm", getScriptsMap("", "Latn")); + scriptsByLocale.put("bmq", getScriptsMap("", "")); + scriptsByLocale.put("bn", getScriptsMap("", "Beng")); + scriptsByLocale.put("bo", getScriptsMap("", "Tibt")); + scriptsByLocale.put("bqi", getScriptsMap("", "")); + scriptsByLocale.put("bqv", getScriptsMap("", "Latn")); + scriptsByLocale.put("br", getScriptsMap("", "Latn")); + scriptsByLocale.put("bra", getScriptsMap("", "Deva")); + scriptsByLocale.put("brh", getScriptsMap("", "")); + scriptsByLocale.put("brx", getScriptsMap("", "Deva")); + scriptsByLocale.put("bs", getScriptsMap("", "Latn")); + scriptsByLocale.put("bss", getScriptsMap("", "")); + scriptsByLocale.put("bto", getScriptsMap("", "")); + scriptsByLocale.put("btv", getScriptsMap("", "Deva")); + scriptsByLocale.put("bua", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("buc", getScriptsMap("", "Latn")); + scriptsByLocale.put("bug", getScriptsMap("", "Latn", "ID", "Bugi")); + scriptsByLocale.put("bum", getScriptsMap("", "")); + scriptsByLocale.put("bvb", getScriptsMap("", "")); + scriptsByLocale.put("bya", getScriptsMap("", "Latn")); + scriptsByLocale.put("byn", getScriptsMap("", "Ethi")); + scriptsByLocale.put("byv", getScriptsMap("", "")); + scriptsByLocale.put("bze", getScriptsMap("", "")); + scriptsByLocale.put("bzx", getScriptsMap("", "")); + scriptsByLocale.put("ca", getScriptsMap("", "Latn")); + scriptsByLocale.put("cad", getScriptsMap("", "Latn")); + scriptsByLocale.put("car", getScriptsMap("", "Latn")); + scriptsByLocale.put("cay", getScriptsMap("", "Latn")); + scriptsByLocale.put("cch", getScriptsMap("", "Latn")); + scriptsByLocale.put("ccp", getScriptsMap("", "Beng")); + scriptsByLocale.put("ce", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ceb", getScriptsMap("", "Latn")); + scriptsByLocale.put("cgg", getScriptsMap("", "Latn")); + scriptsByLocale.put("ch", getScriptsMap("", "Latn")); + scriptsByLocale.put("chk", getScriptsMap("", "Latn")); + scriptsByLocale.put("chm", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("chn", getScriptsMap("", "Latn")); + scriptsByLocale.put("cho", getScriptsMap("", "Latn")); + scriptsByLocale.put("chp", getScriptsMap("", "Latn")); + scriptsByLocale.put("chr", getScriptsMap("", "Cher")); + scriptsByLocale.put("chy", getScriptsMap("", "Latn")); + scriptsByLocale.put("cja", getScriptsMap("", "Arab")); + scriptsByLocale.put("cjm", getScriptsMap("", "Cham")); + scriptsByLocale.put("cjs", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ckb", getScriptsMap("", "Arab")); + scriptsByLocale.put("ckt", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("co", getScriptsMap("", "Latn")); + scriptsByLocale.put("cop", getScriptsMap("", "Arab")); + scriptsByLocale.put("cpe", getScriptsMap("", "Latn")); + scriptsByLocale.put("cr", getScriptsMap("", "Cans")); + scriptsByLocale.put("crh", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("crj", getScriptsMap("", "")); + scriptsByLocale.put("crk", getScriptsMap("", "Cans")); + scriptsByLocale.put("crl", getScriptsMap("", "")); + scriptsByLocale.put("crm", getScriptsMap("", "")); + scriptsByLocale.put("crs", getScriptsMap("", "")); + scriptsByLocale.put("cs", getScriptsMap("", "Latn")); + scriptsByLocale.put("csb", getScriptsMap("", "Latn")); + scriptsByLocale.put("csw", getScriptsMap("", "")); + scriptsByLocale.put("cu", getScriptsMap("", "Glag")); + scriptsByLocale.put("cv", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("cy", getScriptsMap("", "Latn")); + scriptsByLocale.put("da", getScriptsMap("", "Latn")); + scriptsByLocale.put("daf", getScriptsMap("", "")); + scriptsByLocale.put("dak", getScriptsMap("", "Latn")); + scriptsByLocale.put("dar", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("dav", getScriptsMap("", "Latn")); + scriptsByLocale.put("dcc", getScriptsMap("", "")); + scriptsByLocale.put("de", getScriptsMap("", "Latn", "BR", "Runr", "KZ", "Runr", "US", "Runr")); + scriptsByLocale.put("del", getScriptsMap("", "Latn")); + scriptsByLocale.put("den", getScriptsMap("", "Latn")); + scriptsByLocale.put("dgr", getScriptsMap("", "Latn")); + scriptsByLocale.put("din", getScriptsMap("", "Latn")); + scriptsByLocale.put("dje", getScriptsMap("", "Latn")); + scriptsByLocale.put("dng", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("doi", getScriptsMap("", "Arab")); + scriptsByLocale.put("dsb", getScriptsMap("", "Latn")); + scriptsByLocale.put("dtm", getScriptsMap("", "")); + scriptsByLocale.put("dua", getScriptsMap("", "Latn")); + scriptsByLocale.put("dv", getScriptsMap("", "Thaa")); + scriptsByLocale.put("dyo", getScriptsMap("", "Arab")); + scriptsByLocale.put("dyu", getScriptsMap("", "Latn")); + scriptsByLocale.put("dz", getScriptsMap("", "Tibt")); + scriptsByLocale.put("ebu", getScriptsMap("", "Latn")); + scriptsByLocale.put("ee", getScriptsMap("", "Latn")); + scriptsByLocale.put("efi", getScriptsMap("", "Latn")); + scriptsByLocale.put("egy", getScriptsMap("", "Egyp")); + scriptsByLocale.put("eka", getScriptsMap("", "Latn")); + scriptsByLocale.put("eky", getScriptsMap("", "Kali")); + scriptsByLocale.put("el", getScriptsMap("", "Grek")); + scriptsByLocale.put("en", getScriptsMap("", "Latn")); + scriptsByLocale.put("eo", getScriptsMap("", "Latn")); + scriptsByLocale.put("es", getScriptsMap("", "Latn")); + scriptsByLocale.put("et", getScriptsMap("", "Latn")); + scriptsByLocale.put("ett", getScriptsMap("", "Ital")); + scriptsByLocale.put("eu", getScriptsMap("", "Latn")); + scriptsByLocale.put("evn", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ewo", getScriptsMap("", "Latn")); + scriptsByLocale.put("fa", getScriptsMap("", "Arab")); + scriptsByLocale.put("fan", getScriptsMap("", "Latn")); + scriptsByLocale.put("ff", getScriptsMap("", "Latn")); + scriptsByLocale.put("ffm", getScriptsMap("", "")); + scriptsByLocale.put("fi", getScriptsMap("", "Latn")); + scriptsByLocale.put("fil", getScriptsMap("", "Latn", "US", "Tglg")); + scriptsByLocale.put("fiu", getScriptsMap("", "Latn")); + scriptsByLocale.put("fj", getScriptsMap("", "Latn")); + scriptsByLocale.put("fo", getScriptsMap("", "Latn")); + scriptsByLocale.put("fon", getScriptsMap("", "Latn")); + scriptsByLocale.put("fr", getScriptsMap("", "Latn")); + scriptsByLocale.put("frr", getScriptsMap("", "Latn")); + scriptsByLocale.put("frs", getScriptsMap("", "Latn")); + scriptsByLocale.put("fud", getScriptsMap("", "")); + scriptsByLocale.put("fuq", getScriptsMap("", "")); + scriptsByLocale.put("fur", getScriptsMap("", "Latn")); + scriptsByLocale.put("fuv", getScriptsMap("", "")); + scriptsByLocale.put("fy", getScriptsMap("", "Latn")); + scriptsByLocale.put("ga", getScriptsMap("", "Latn")); + scriptsByLocale.put("gaa", getScriptsMap("", "Latn")); + scriptsByLocale.put("gag", getScriptsMap("", "Latn", "MD", "Cyrl")); + scriptsByLocale.put("gay", getScriptsMap("", "Latn")); + scriptsByLocale.put("gba", getScriptsMap("", "Arab")); + scriptsByLocale.put("gbm", getScriptsMap("", "Deva")); + scriptsByLocale.put("gcr", getScriptsMap("", "Latn")); + scriptsByLocale.put("gd", getScriptsMap("", "Latn")); + scriptsByLocale.put("gez", getScriptsMap("", "Ethi")); + scriptsByLocale.put("ggn", getScriptsMap("", "")); + scriptsByLocale.put("gil", getScriptsMap("", "Latn")); + scriptsByLocale.put("gjk", getScriptsMap("", "")); + scriptsByLocale.put("gju", getScriptsMap("", "")); + scriptsByLocale.put("gl", getScriptsMap("", "Latn")); + scriptsByLocale.put("gld", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("glk", getScriptsMap("", "")); + scriptsByLocale.put("gn", getScriptsMap("", "Latn")); + scriptsByLocale.put("gon", getScriptsMap("", "Telu")); + scriptsByLocale.put("gor", getScriptsMap("", "Latn")); + scriptsByLocale.put("gos", getScriptsMap("", "")); + scriptsByLocale.put("got", getScriptsMap("", "Goth")); + scriptsByLocale.put("grb", getScriptsMap("", "Latn")); + scriptsByLocale.put("grc", getScriptsMap("", "Cprt")); + scriptsByLocale.put("grt", getScriptsMap("", "Beng")); + scriptsByLocale.put("gsw", getScriptsMap("", "Latn")); + scriptsByLocale.put("gu", getScriptsMap("", "Gujr")); + scriptsByLocale.put("gub", getScriptsMap("", "")); + scriptsByLocale.put("guz", getScriptsMap("", "Latn")); + scriptsByLocale.put("gv", getScriptsMap("", "Latn")); + scriptsByLocale.put("gvr", getScriptsMap("", "")); + scriptsByLocale.put("gwi", getScriptsMap("", "Latn")); + scriptsByLocale.put("ha", getScriptsMap("", "Arab", "NE", "Latn", "GH", "Latn")); + scriptsByLocale.put("hai", getScriptsMap("", "Latn")); + scriptsByLocale.put("haw", getScriptsMap("", "Latn")); + scriptsByLocale.put("haz", getScriptsMap("", "")); + scriptsByLocale.put("he", getScriptsMap("", "Hebr")); + scriptsByLocale.put("hi", getScriptsMap("", "Deva")); + scriptsByLocale.put("hil", getScriptsMap("", "Latn")); + scriptsByLocale.put("hit", getScriptsMap("", "Xsux")); + scriptsByLocale.put("hmn", getScriptsMap("", "Latn")); + scriptsByLocale.put("hnd", getScriptsMap("", "")); + scriptsByLocale.put("hne", getScriptsMap("", "Deva")); + scriptsByLocale.put("hnn", getScriptsMap("", "Latn")); + scriptsByLocale.put("hno", getScriptsMap("", "")); + scriptsByLocale.put("ho", getScriptsMap("", "Latn")); + scriptsByLocale.put("hoc", getScriptsMap("", "Deva")); + scriptsByLocale.put("hoj", getScriptsMap("", "Deva")); + scriptsByLocale.put("hop", getScriptsMap("", "Latn")); + scriptsByLocale.put("hr", getScriptsMap("", "Latn")); + scriptsByLocale.put("hsb", getScriptsMap("", "Latn")); + scriptsByLocale.put("ht", getScriptsMap("", "Latn")); + scriptsByLocale.put("hu", getScriptsMap("", "Latn")); + scriptsByLocale.put("hup", getScriptsMap("", "Latn")); + scriptsByLocale.put("hy", getScriptsMap("", "Armn")); + scriptsByLocale.put("hz", getScriptsMap("", "Latn")); + scriptsByLocale.put("ia", getScriptsMap("", "Latn")); + scriptsByLocale.put("iba", getScriptsMap("", "Latn")); + scriptsByLocale.put("ibb", getScriptsMap("", "Latn")); + scriptsByLocale.put("id", getScriptsMap("", "Latn")); + scriptsByLocale.put("ig", getScriptsMap("", "Latn")); + scriptsByLocale.put("ii", getScriptsMap("", "Yiii", "CN", "Latn")); + scriptsByLocale.put("ik", getScriptsMap("", "Latn")); + scriptsByLocale.put("ikt", getScriptsMap("", "")); + scriptsByLocale.put("ilo", getScriptsMap("", "Latn")); + scriptsByLocale.put("inh", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("is", getScriptsMap("", "Latn")); + scriptsByLocale.put("it", getScriptsMap("", "Latn")); + scriptsByLocale.put("iu", getScriptsMap("", "Cans", "CA", "Latn")); + scriptsByLocale.put("ja", getScriptsMap("", "Jpan")); + scriptsByLocale.put("jmc", getScriptsMap("", "Latn")); + scriptsByLocale.put("jml", getScriptsMap("", "")); + scriptsByLocale.put("jpr", getScriptsMap("", "Hebr")); + scriptsByLocale.put("jrb", getScriptsMap("", "Hebr")); + scriptsByLocale.put("jv", getScriptsMap("", "Latn", "ID", "Java")); + scriptsByLocale.put("ka", getScriptsMap("", "Geor")); + scriptsByLocale.put("kaa", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kab", getScriptsMap("", "Latn")); + scriptsByLocale.put("kac", getScriptsMap("", "Latn")); + scriptsByLocale.put("kaj", getScriptsMap("", "Latn")); + scriptsByLocale.put("kam", getScriptsMap("", "Latn")); + scriptsByLocale.put("kao", getScriptsMap("", "")); + scriptsByLocale.put("kbd", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kca", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kcg", getScriptsMap("", "Latn")); + scriptsByLocale.put("kck", getScriptsMap("", "")); + scriptsByLocale.put("kde", getScriptsMap("", "Latn")); + scriptsByLocale.put("kdt", getScriptsMap("", "Thai")); + scriptsByLocale.put("kea", getScriptsMap("", "Latn")); + scriptsByLocale.put("kfo", getScriptsMap("", "Latn")); + scriptsByLocale.put("kfr", getScriptsMap("", "Deva")); + scriptsByLocale.put("kfy", getScriptsMap("", "")); + scriptsByLocale.put("kg", getScriptsMap("", "Latn")); + scriptsByLocale.put("kge", getScriptsMap("", "")); + scriptsByLocale.put("kgp", getScriptsMap("", "")); + scriptsByLocale.put("kha", getScriptsMap("", "Latn", "IN", "Beng")); + scriptsByLocale.put("khb", getScriptsMap("", "Talu")); + scriptsByLocale.put("khn", getScriptsMap("", "")); + scriptsByLocale.put("khq", getScriptsMap("", "Latn")); + scriptsByLocale.put("kht", getScriptsMap("", "Mymr")); + scriptsByLocale.put("khw", getScriptsMap("", "")); + scriptsByLocale.put("ki", getScriptsMap("", "Latn")); + scriptsByLocale.put("kj", getScriptsMap("", "Latn")); + scriptsByLocale.put("kjg", getScriptsMap("", "")); + scriptsByLocale.put("kjh", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kk", getScriptsMap("", "Arab", "KZ", "Cyrl", "TR", "Cyrl")); + scriptsByLocale.put("kkj", getScriptsMap("", "")); + scriptsByLocale.put("kl", getScriptsMap("", "Latn")); + scriptsByLocale.put("kln", getScriptsMap("", "Latn")); + scriptsByLocale.put("km", getScriptsMap("", "Khmr")); + scriptsByLocale.put("kmb", getScriptsMap("", "Latn")); + scriptsByLocale.put("kn", getScriptsMap("", "Knda")); + scriptsByLocale.put("ko", getScriptsMap("", "Kore")); + scriptsByLocale.put("koi", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kok", getScriptsMap("", "Deva")); + scriptsByLocale.put("kos", getScriptsMap("", "Latn")); + scriptsByLocale.put("kpe", getScriptsMap("", "Latn")); + scriptsByLocale.put("kpy", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kr", getScriptsMap("", "Latn")); + scriptsByLocale.put("krc", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kri", getScriptsMap("", "Latn")); + scriptsByLocale.put("krl", getScriptsMap("", "Latn")); + scriptsByLocale.put("kru", getScriptsMap("", "Deva")); + scriptsByLocale.put("ks", getScriptsMap("", "Arab")); + scriptsByLocale.put("ksb", getScriptsMap("", "Latn")); + scriptsByLocale.put("ksf", getScriptsMap("", "Latn")); + scriptsByLocale.put("ksh", getScriptsMap("", "Latn")); + scriptsByLocale.put("ku", getScriptsMap("", "Latn", "LB", "Arab")); + scriptsByLocale.put("kum", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kut", getScriptsMap("", "Latn")); + scriptsByLocale.put("kv", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("kvr", getScriptsMap("", "")); + scriptsByLocale.put("kvx", getScriptsMap("", "")); + scriptsByLocale.put("kw", getScriptsMap("", "Latn")); + scriptsByLocale.put("kxm", getScriptsMap("", "")); + scriptsByLocale.put("kxp", getScriptsMap("", "")); + scriptsByLocale.put("ky", getScriptsMap("", "Cyrl", "CN", "Arab", "TR", "Latn")); + scriptsByLocale.put("kyu", getScriptsMap("", "Kali")); + scriptsByLocale.put("la", getScriptsMap("", "Latn")); + scriptsByLocale.put("lad", getScriptsMap("", "Hebr")); + scriptsByLocale.put("lag", getScriptsMap("", "Latn")); + scriptsByLocale.put("lah", getScriptsMap("", "Arab")); + scriptsByLocale.put("laj", getScriptsMap("", "")); + scriptsByLocale.put("lam", getScriptsMap("", "Latn")); + scriptsByLocale.put("lb", getScriptsMap("", "Latn")); + scriptsByLocale.put("lbe", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("lbw", getScriptsMap("", "")); + scriptsByLocale.put("lcp", getScriptsMap("", "Thai")); + scriptsByLocale.put("lep", getScriptsMap("", "Lepc")); + scriptsByLocale.put("lez", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("lg", getScriptsMap("", "Latn")); + scriptsByLocale.put("li", getScriptsMap("", "Latn")); + scriptsByLocale.put("lif", getScriptsMap("", "Deva")); + scriptsByLocale.put("lis", getScriptsMap("", "Lisu")); + scriptsByLocale.put("ljp", getScriptsMap("", "")); + scriptsByLocale.put("lki", getScriptsMap("", "Arab")); + scriptsByLocale.put("lkt", getScriptsMap("", "")); + scriptsByLocale.put("lmn", getScriptsMap("", "Telu")); + scriptsByLocale.put("lmo", getScriptsMap("", "")); + scriptsByLocale.put("ln", getScriptsMap("", "Latn")); + scriptsByLocale.put("lo", getScriptsMap("", "Laoo")); + scriptsByLocale.put("lol", getScriptsMap("", "Latn")); + scriptsByLocale.put("loz", getScriptsMap("", "Latn")); + scriptsByLocale.put("lrc", getScriptsMap("", "")); + scriptsByLocale.put("lt", getScriptsMap("", "Latn")); + scriptsByLocale.put("lu", getScriptsMap("", "Latn")); + scriptsByLocale.put("lua", getScriptsMap("", "Latn")); + scriptsByLocale.put("lui", getScriptsMap("", "Latn")); + scriptsByLocale.put("lun", getScriptsMap("", "Latn")); + scriptsByLocale.put("luo", getScriptsMap("", "Latn")); + scriptsByLocale.put("lus", getScriptsMap("", "Beng")); + scriptsByLocale.put("lut", getScriptsMap("", "Latn")); + scriptsByLocale.put("luy", getScriptsMap("", "Latn")); + scriptsByLocale.put("luz", getScriptsMap("", "")); + scriptsByLocale.put("lv", getScriptsMap("", "Latn")); + scriptsByLocale.put("lwl", getScriptsMap("", "Thai")); + scriptsByLocale.put("mad", getScriptsMap("", "Latn")); + scriptsByLocale.put("maf", getScriptsMap("", "")); + scriptsByLocale.put("mag", getScriptsMap("", "Deva")); + scriptsByLocale.put("mai", getScriptsMap("", "Deva")); + scriptsByLocale.put("mak", getScriptsMap("", "Latn", "ID", "Bugi")); + scriptsByLocale.put("man", getScriptsMap("", "Latn", "GN", "Nkoo")); + scriptsByLocale.put("mas", getScriptsMap("", "Latn")); + scriptsByLocale.put("maz", getScriptsMap("", "")); + scriptsByLocale.put("mdf", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("mdh", getScriptsMap("", "Latn")); + scriptsByLocale.put("mdr", getScriptsMap("", "Latn")); + scriptsByLocale.put("mdt", getScriptsMap("", "")); + scriptsByLocale.put("men", getScriptsMap("", "Latn")); + scriptsByLocale.put("mer", getScriptsMap("", "Latn")); + scriptsByLocale.put("mfa", getScriptsMap("", "")); + scriptsByLocale.put("mfe", getScriptsMap("", "Latn")); + scriptsByLocale.put("mg", getScriptsMap("", "Latn")); + scriptsByLocale.put("mgh", getScriptsMap("", "Latn")); + scriptsByLocale.put("mgp", getScriptsMap("", "")); + scriptsByLocale.put("mgy", getScriptsMap("", "")); + scriptsByLocale.put("mh", getScriptsMap("", "Latn")); + scriptsByLocale.put("mi", getScriptsMap("", "Latn")); + scriptsByLocale.put("mic", getScriptsMap("", "Latn")); + scriptsByLocale.put("min", getScriptsMap("", "Latn")); + scriptsByLocale.put("mk", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ml", getScriptsMap("", "Mlym")); + scriptsByLocale.put("mn", getScriptsMap("", "Cyrl", "CN", "Mong")); + scriptsByLocale.put("mnc", getScriptsMap("", "Mong")); + scriptsByLocale.put("mni", getScriptsMap("", "Beng", "IN", "Mtei")); + scriptsByLocale.put("mns", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("mnw", getScriptsMap("", "Mymr")); + scriptsByLocale.put("moe", getScriptsMap("", "")); + scriptsByLocale.put("moh", getScriptsMap("", "Latn")); + scriptsByLocale.put("mos", getScriptsMap("", "Latn")); + scriptsByLocale.put("mr", getScriptsMap("", "Deva")); + scriptsByLocale.put("mrd", getScriptsMap("", "")); + scriptsByLocale.put("mrj", getScriptsMap("", "")); + scriptsByLocale.put("ms", getScriptsMap("", "Arab", "MY", "Latn", "SG", "Latn")); + scriptsByLocale.put("mt", getScriptsMap("", "Latn")); + scriptsByLocale.put("mtr", getScriptsMap("", "")); + scriptsByLocale.put("mua", getScriptsMap("", "Latn")); + scriptsByLocale.put("mus", getScriptsMap("", "Latn")); + scriptsByLocale.put("mvy", getScriptsMap("", "")); + scriptsByLocale.put("mwk", getScriptsMap("", "")); + scriptsByLocale.put("mwl", getScriptsMap("", "Latn")); + scriptsByLocale.put("mwr", getScriptsMap("", "Deva")); + scriptsByLocale.put("mxc", getScriptsMap("", "")); + scriptsByLocale.put("my", getScriptsMap("", "Mymr")); + scriptsByLocale.put("myv", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("myx", getScriptsMap("", "")); + scriptsByLocale.put("myz", getScriptsMap("", "Mand")); + scriptsByLocale.put("na", getScriptsMap("", "Latn")); + scriptsByLocale.put("nap", getScriptsMap("", "Latn")); + scriptsByLocale.put("naq", getScriptsMap("", "Latn")); + scriptsByLocale.put("nb", getScriptsMap("", "Latn")); + scriptsByLocale.put("nbf", getScriptsMap("", "")); + scriptsByLocale.put("nch", getScriptsMap("", "")); + scriptsByLocale.put("nd", getScriptsMap("", "Latn")); + scriptsByLocale.put("ndc", getScriptsMap("", "")); + scriptsByLocale.put("nds", getScriptsMap("", "Latn")); + scriptsByLocale.put("ne", getScriptsMap("", "Deva")); + scriptsByLocale.put("new", getScriptsMap("", "Deva")); + scriptsByLocale.put("ng", getScriptsMap("", "Latn")); + scriptsByLocale.put("ngl", getScriptsMap("", "")); + scriptsByLocale.put("nhe", getScriptsMap("", "")); + scriptsByLocale.put("nhw", getScriptsMap("", "")); + scriptsByLocale.put("nia", getScriptsMap("", "Latn")); + scriptsByLocale.put("nij", getScriptsMap("", "")); + scriptsByLocale.put("niu", getScriptsMap("", "Latn")); + scriptsByLocale.put("nl", getScriptsMap("", "Latn")); + scriptsByLocale.put("nmg", getScriptsMap("", "Latn")); + scriptsByLocale.put("nn", getScriptsMap("", "Latn")); + scriptsByLocale.put("nnh", getScriptsMap("", "")); + scriptsByLocale.put("nod", getScriptsMap("", "Lana")); + scriptsByLocale.put("noe", getScriptsMap("", "")); + scriptsByLocale.put("nog", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("nqo", getScriptsMap("", "Nkoo")); + scriptsByLocale.put("nr", getScriptsMap("", "Latn")); + scriptsByLocale.put("nsk", getScriptsMap("", "")); + scriptsByLocale.put("nso", getScriptsMap("", "Latn")); + scriptsByLocale.put("nus", getScriptsMap("", "Latn")); + scriptsByLocale.put("nv", getScriptsMap("", "Latn")); + scriptsByLocale.put("ny", getScriptsMap("", "Latn")); + scriptsByLocale.put("nym", getScriptsMap("", "Latn")); + scriptsByLocale.put("nyn", getScriptsMap("", "Latn")); + scriptsByLocale.put("nyo", getScriptsMap("", "Latn")); + scriptsByLocale.put("nzi", getScriptsMap("", "Latn")); + scriptsByLocale.put("oc", getScriptsMap("", "Latn")); + scriptsByLocale.put("oj", getScriptsMap("", "Cans")); + scriptsByLocale.put("om", getScriptsMap("", "Latn", "ET", "Ethi")); + scriptsByLocale.put("or", getScriptsMap("", "Orya")); + scriptsByLocale.put("os", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("osa", getScriptsMap("", "Latn")); + scriptsByLocale.put("osc", getScriptsMap("", "Ital")); + scriptsByLocale.put("otk", getScriptsMap("", "Orkh")); + scriptsByLocale.put("pa", getScriptsMap("", "Guru", "PK", "Arab")); + scriptsByLocale.put("pag", getScriptsMap("", "Latn")); + scriptsByLocale.put("pal", getScriptsMap("", "Phli")); + scriptsByLocale.put("pam", getScriptsMap("", "Latn")); + scriptsByLocale.put("pap", getScriptsMap("", "Latn")); + scriptsByLocale.put("pau", getScriptsMap("", "Latn")); + scriptsByLocale.put("peo", getScriptsMap("", "Xpeo")); + scriptsByLocale.put("phn", getScriptsMap("", "Phnx")); + scriptsByLocale.put("pi", getScriptsMap("", "Deva")); + scriptsByLocale.put("pko", getScriptsMap("", "")); + scriptsByLocale.put("pl", getScriptsMap("", "Latn")); + scriptsByLocale.put("pon", getScriptsMap("", "Latn")); + scriptsByLocale.put("pra", getScriptsMap("", "Brah")); + scriptsByLocale.put("prd", getScriptsMap("", "Arab")); + scriptsByLocale.put("prg", getScriptsMap("", "Latn")); + scriptsByLocale.put("prs", getScriptsMap("", "Arab")); + scriptsByLocale.put("ps", getScriptsMap("", "Arab")); + scriptsByLocale.put("pt", getScriptsMap("", "Latn")); + scriptsByLocale.put("puu", getScriptsMap("", "")); + scriptsByLocale.put("qu", getScriptsMap("", "Latn")); + scriptsByLocale.put("raj", getScriptsMap("", "Latn")); + scriptsByLocale.put("rap", getScriptsMap("", "Latn")); + scriptsByLocale.put("rar", getScriptsMap("", "Latn")); + scriptsByLocale.put("rcf", getScriptsMap("", "Latn")); + scriptsByLocale.put("rej", getScriptsMap("", "Latn", "ID", "Rjng")); + scriptsByLocale.put("ria", getScriptsMap("", "")); + scriptsByLocale.put("rif", getScriptsMap("", "")); + scriptsByLocale.put("rjs", getScriptsMap("", "Deva")); + scriptsByLocale.put("rkt", getScriptsMap("", "Beng")); + scriptsByLocale.put("rm", getScriptsMap("", "Latn")); + scriptsByLocale.put("rmf", getScriptsMap("", "")); + scriptsByLocale.put("rmo", getScriptsMap("", "")); + scriptsByLocale.put("rmt", getScriptsMap("", "")); + scriptsByLocale.put("rn", getScriptsMap("", "Latn")); + scriptsByLocale.put("rng", getScriptsMap("", "")); + scriptsByLocale.put("ro", getScriptsMap("", "Latn", "RS", "Cyrl")); + scriptsByLocale.put("rob", getScriptsMap("", "")); + scriptsByLocale.put("rof", getScriptsMap("", "Latn")); + scriptsByLocale.put("rom", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ru", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("rue", getScriptsMap("", "")); + scriptsByLocale.put("rup", getScriptsMap("", "Latn")); + scriptsByLocale.put("rw", getScriptsMap("", "Latn")); + scriptsByLocale.put("rwk", getScriptsMap("", "Latn")); + scriptsByLocale.put("ryu", getScriptsMap("", "")); + scriptsByLocale.put("sa", getScriptsMap("", "Deva")); + scriptsByLocale.put("sad", getScriptsMap("", "Latn")); + scriptsByLocale.put("saf", getScriptsMap("", "Latn")); + scriptsByLocale.put("sah", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("sam", getScriptsMap("", "Hebr")); + scriptsByLocale.put("saq", getScriptsMap("", "Latn")); + scriptsByLocale.put("sas", getScriptsMap("", "Latn")); + scriptsByLocale.put("sat", getScriptsMap("", "Latn")); + scriptsByLocale.put("saz", getScriptsMap("", "Saur")); + scriptsByLocale.put("sbp", getScriptsMap("", "Latn")); + scriptsByLocale.put("sc", getScriptsMap("", "Latn")); + scriptsByLocale.put("sck", getScriptsMap("", "")); + scriptsByLocale.put("scn", getScriptsMap("", "Latn")); + scriptsByLocale.put("sco", getScriptsMap("", "Latn")); + scriptsByLocale.put("scs", getScriptsMap("", "")); + scriptsByLocale.put("sd", getScriptsMap("", "Arab", "IN", "Deva")); + scriptsByLocale.put("sdh", getScriptsMap("", "Arab")); + scriptsByLocale.put("se", getScriptsMap("", "Latn", "NO", "Cyrl")); + scriptsByLocale.put("see", getScriptsMap("", "Latn")); + scriptsByLocale.put("sef", getScriptsMap("", "")); + scriptsByLocale.put("seh", getScriptsMap("", "Latn")); + scriptsByLocale.put("sel", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ses", getScriptsMap("", "Latn")); + scriptsByLocale.put("sg", getScriptsMap("", "Latn")); + scriptsByLocale.put("sga", getScriptsMap("", "Latn")); + scriptsByLocale.put("shi", getScriptsMap("", "Tfng")); + scriptsByLocale.put("shn", getScriptsMap("", "Mymr")); + scriptsByLocale.put("si", getScriptsMap("", "Sinh")); + scriptsByLocale.put("sid", getScriptsMap("", "Latn")); + scriptsByLocale.put("sk", getScriptsMap("", "Latn")); + scriptsByLocale.put("skr", getScriptsMap("", "")); + scriptsByLocale.put("sl", getScriptsMap("", "Latn")); + scriptsByLocale.put("sm", getScriptsMap("", "Latn")); + scriptsByLocale.put("sma", getScriptsMap("", "Latn")); + scriptsByLocale.put("smi", getScriptsMap("", "Latn")); + scriptsByLocale.put("smj", getScriptsMap("", "Latn")); + scriptsByLocale.put("smn", getScriptsMap("", "Latn")); + scriptsByLocale.put("sms", getScriptsMap("", "Latn")); + scriptsByLocale.put("sn", getScriptsMap("", "Latn")); + scriptsByLocale.put("snk", getScriptsMap("", "Latn")); + scriptsByLocale.put("so", getScriptsMap("", "Latn")); + scriptsByLocale.put("son", getScriptsMap("", "Latn")); + scriptsByLocale.put("sou", getScriptsMap("", "")); + scriptsByLocale.put("sq", getScriptsMap("", "Latn")); + scriptsByLocale.put("sr", getScriptsMap("", "Latn")); + scriptsByLocale.put("srn", getScriptsMap("", "Latn")); + scriptsByLocale.put("srr", getScriptsMap("", "Latn")); + scriptsByLocale.put("srx", getScriptsMap("", "")); + scriptsByLocale.put("ss", getScriptsMap("", "Latn")); + scriptsByLocale.put("ssy", getScriptsMap("", "Latn")); + scriptsByLocale.put("st", getScriptsMap("", "Latn")); + scriptsByLocale.put("su", getScriptsMap("", "Latn")); + scriptsByLocale.put("suk", getScriptsMap("", "Latn")); + scriptsByLocale.put("sus", getScriptsMap("", "Latn", "GN", "Arab")); + scriptsByLocale.put("sv", getScriptsMap("", "Latn")); + scriptsByLocale.put("sw", getScriptsMap("", "Latn")); + scriptsByLocale.put("swb", getScriptsMap("", "Arab", "YT", "Latn")); + scriptsByLocale.put("swc", getScriptsMap("", "Latn")); + scriptsByLocale.put("swv", getScriptsMap("", "")); + scriptsByLocale.put("sxn", getScriptsMap("", "")); + scriptsByLocale.put("syi", getScriptsMap("", "")); + scriptsByLocale.put("syl", getScriptsMap("", "Beng", "BD", "Sylo")); + scriptsByLocale.put("syr", getScriptsMap("", "Syrc")); + scriptsByLocale.put("ta", getScriptsMap("", "Taml")); + scriptsByLocale.put("tab", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("taj", getScriptsMap("", "")); + scriptsByLocale.put("tbw", getScriptsMap("", "Latn")); + scriptsByLocale.put("tcy", getScriptsMap("", "Knda")); + scriptsByLocale.put("tdd", getScriptsMap("", "Tale")); + scriptsByLocale.put("tdg", getScriptsMap("", "")); + scriptsByLocale.put("tdh", getScriptsMap("", "")); + scriptsByLocale.put("te", getScriptsMap("", "Telu")); + scriptsByLocale.put("tem", getScriptsMap("", "Latn")); + scriptsByLocale.put("teo", getScriptsMap("", "Latn")); + scriptsByLocale.put("ter", getScriptsMap("", "Latn")); + scriptsByLocale.put("tet", getScriptsMap("", "Latn")); + scriptsByLocale.put("tg", getScriptsMap("", "Cyrl", "PK", "Arab")); + scriptsByLocale.put("th", getScriptsMap("", "Thai")); + scriptsByLocale.put("thl", getScriptsMap("", "")); + scriptsByLocale.put("thq", getScriptsMap("", "")); + scriptsByLocale.put("thr", getScriptsMap("", "")); + scriptsByLocale.put("ti", getScriptsMap("", "Ethi")); + scriptsByLocale.put("tig", getScriptsMap("", "Ethi")); + scriptsByLocale.put("tiv", getScriptsMap("", "Latn")); + scriptsByLocale.put("tk", getScriptsMap("", "Latn")); + scriptsByLocale.put("tkl", getScriptsMap("", "Latn")); + scriptsByLocale.put("tkt", getScriptsMap("", "")); + scriptsByLocale.put("tli", getScriptsMap("", "Latn")); + scriptsByLocale.put("tmh", getScriptsMap("", "Latn")); + scriptsByLocale.put("tn", getScriptsMap("", "Latn")); + scriptsByLocale.put("to", getScriptsMap("", "Latn")); + scriptsByLocale.put("tog", getScriptsMap("", "Latn")); + scriptsByLocale.put("tpi", getScriptsMap("", "Latn")); + scriptsByLocale.put("tr", getScriptsMap("", "Latn", "DE", "Arab", "MK", "Arab")); + scriptsByLocale.put("tru", getScriptsMap("", "Latn")); + scriptsByLocale.put("trv", getScriptsMap("", "Latn")); + scriptsByLocale.put("ts", getScriptsMap("", "Latn")); + scriptsByLocale.put("tsf", getScriptsMap("", "")); + scriptsByLocale.put("tsg", getScriptsMap("", "Latn")); + scriptsByLocale.put("tsi", getScriptsMap("", "Latn")); + scriptsByLocale.put("tsj", getScriptsMap("", "")); + scriptsByLocale.put("tt", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("ttj", getScriptsMap("", "")); + scriptsByLocale.put("tts", getScriptsMap("", "Thai")); + scriptsByLocale.put("tum", getScriptsMap("", "Latn")); + scriptsByLocale.put("tut", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("tvl", getScriptsMap("", "Latn")); + scriptsByLocale.put("twq", getScriptsMap("", "Latn")); + scriptsByLocale.put("ty", getScriptsMap("", "Latn")); + scriptsByLocale.put("tyv", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("tzm", getScriptsMap("", "Latn")); + scriptsByLocale.put("ude", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("udm", getScriptsMap("", "Cyrl", "RU", "Latn")); + scriptsByLocale.put("ug", getScriptsMap("", "Arab", "KZ", "Cyrl", "MN", "Cyrl")); + scriptsByLocale.put("uga", getScriptsMap("", "Ugar")); + scriptsByLocale.put("uk", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("uli", getScriptsMap("", "Latn")); + scriptsByLocale.put("umb", getScriptsMap("", "Latn")); + scriptsByLocale.put("und", getScriptsMap("", "")); + scriptsByLocale.put("unr", getScriptsMap("", "Beng", "NP", "Deva")); + scriptsByLocale.put("unx", getScriptsMap("", "Beng")); + scriptsByLocale.put("ur", getScriptsMap("", "Arab")); + scriptsByLocale.put("uz", getScriptsMap("", "Latn", "AF", "Arab", "CN", "Cyrl")); + scriptsByLocale.put("vai", getScriptsMap("", "Vaii")); + scriptsByLocale.put("ve", getScriptsMap("", "Latn")); + scriptsByLocale.put("vi", getScriptsMap("", "Latn", "US", "Hani")); + scriptsByLocale.put("vic", getScriptsMap("", "")); + scriptsByLocale.put("vmw", getScriptsMap("", "")); + scriptsByLocale.put("vo", getScriptsMap("", "Latn")); + scriptsByLocale.put("vot", getScriptsMap("", "Latn")); + scriptsByLocale.put("vun", getScriptsMap("", "Latn")); + scriptsByLocale.put("wa", getScriptsMap("", "Latn")); + scriptsByLocale.put("wae", getScriptsMap("", "Latn")); + scriptsByLocale.put("wak", getScriptsMap("", "Latn")); + scriptsByLocale.put("wal", getScriptsMap("", "Ethi")); + scriptsByLocale.put("war", getScriptsMap("", "Latn")); + scriptsByLocale.put("was", getScriptsMap("", "Latn")); + scriptsByLocale.put("wbq", getScriptsMap("", "")); + scriptsByLocale.put("wbr", getScriptsMap("", "")); + scriptsByLocale.put("wls", getScriptsMap("", "")); + scriptsByLocale.put("wo", getScriptsMap("", "Latn")); + scriptsByLocale.put("wtm", getScriptsMap("", "")); + scriptsByLocale.put("xal", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("xav", getScriptsMap("", "")); + scriptsByLocale.put("xcr", getScriptsMap("", "Cari")); + scriptsByLocale.put("xh", getScriptsMap("", "Latn")); + scriptsByLocale.put("xnr", getScriptsMap("", "")); + scriptsByLocale.put("xog", getScriptsMap("", "Latn")); + scriptsByLocale.put("xpr", getScriptsMap("", "Prti")); + scriptsByLocale.put("xsa", getScriptsMap("", "Sarb")); + scriptsByLocale.put("xsr", getScriptsMap("", "Deva")); + scriptsByLocale.put("xum", getScriptsMap("", "Ital")); + scriptsByLocale.put("yao", getScriptsMap("", "Latn")); + scriptsByLocale.put("yap", getScriptsMap("", "Latn")); + scriptsByLocale.put("yav", getScriptsMap("", "Latn")); + scriptsByLocale.put("ybb", getScriptsMap("", "")); + scriptsByLocale.put("yi", getScriptsMap("", "Hebr")); + scriptsByLocale.put("yo", getScriptsMap("", "Latn")); + scriptsByLocale.put("yrk", getScriptsMap("", "Cyrl")); + scriptsByLocale.put("yua", getScriptsMap("", "")); + scriptsByLocale.put("yue", getScriptsMap("", "Hans")); + scriptsByLocale.put("za", getScriptsMap("", "Latn", "CN", "Hans")); + scriptsByLocale.put("zap", getScriptsMap("", "Latn")); + scriptsByLocale.put("zdj", getScriptsMap("", "")); + scriptsByLocale.put("zea", getScriptsMap("", "")); + scriptsByLocale.put("zen", getScriptsMap("", "Tfng")); + scriptsByLocale.put("zh", getScriptsMap("", "Hant", "CN", "Hans", "HK", "Hans", "MO", "Hans", "SG", "Hans", "MN", "Hans")); + scriptsByLocale.put("zmi", getScriptsMap("", "")); + scriptsByLocale.put("zu", getScriptsMap("", "Latn")); + scriptsByLocale.put("zun", getScriptsMap("", "Latn")); + scriptsByLocale.put("zza", getScriptsMap("", "Arab")); + } + + /** + * Gets the script (writing type) for the given locale. For example, if a US citizen uses German Locale, + * and calls this method with Locale.getDefault(), the result would be "Runr" + * + * @param locale + * @return + */ + public static String getScript(Locale locale) { + String localeString = locale.toString(); + String language = ""; + String country = ""; + if (localeString.contains("_")) { + String[] split = localeString.split("_"); + language = split[0]; + country = split[1]; + } else language = localeString; + + Map scripts = scriptsByLocale.get(language); + String script = scripts.get(country); + return script == null ? scripts.get("") : script; + + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LocaleUtility.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LocaleUtility.java new file mode 100644 index 0000000..dc92941 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/LocaleUtility.java @@ -0,0 +1,57 @@ +package com.liskovsoft.leankeyboard.utils; + +import android.content.Context; +import android.content.res.Configuration; +import android.os.Build.VERSION; +import android.util.Log; + +import java.util.Locale; + +public class LocaleUtility extends LocaleScript { + private static final String TAG = LocaleUtility.class.getSimpleName(); + + public static Locale getSystemLocale(Context context) { + return getSystemLocale(context.getResources().getConfiguration()); + } + + public static void setSystemLocale(Context context, Locale locale) { + setSystemLocale(context.getResources().getConfiguration(), locale); + } + + @SuppressWarnings("deprecation") + public static void setSystemLocale(Configuration config, Locale locale) { + if (VERSION.SDK_INT < 24) { + config.locale = locale; + } else { + config.setLocale(locale); + } + } + + @SuppressWarnings("deprecation") + public static Locale getSystemLocale(Configuration config) { + if (VERSION.SDK_INT < 24) { + return config.locale; + } else { + return config.getLocales().get(0); + } + } + + /** + * Modern Solution + */ + @SuppressWarnings("deprecation") + public static void forceLocaleOld(Context ctx, Locale locale) { + Locale.setDefault(locale); + Configuration config = ctx.getResources().getConfiguration(); + LocaleUtility.setSystemLocale(config, locale); + ctx.getResources().updateConfiguration(config, + ctx.getResources().getDisplayMetrics()); + } + + public static void switchRuLocale(Context ctx) { + Log.d(TAG, "Trying to switch locale back and forward"); + Locale savedLocale = Locale.getDefault(); + LocaleUtility.forceLocaleOld(ctx, new Locale("ru")); + LocaleUtility.forceLocaleOld(ctx, savedLocale); + } +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/TextDrawable.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/TextDrawable.java new file mode 100644 index 0000000..51f1c53 --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/utils/TextDrawable.java @@ -0,0 +1,510 @@ +/** + * Example usage: https://github.com/devunwired/textdrawable/blob/master/sample/src/main/java/com/example/textdrawable/MyActivity.java + */ + +/** + * Copyright (c) 2012 Wireless Designs, LLC + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.liskovsoft.leankeyboard.utils; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.TypedValue; +import androidx.annotation.NonNull; + +/** + * A Drawable object that draws text. + * A TextDrawable accepts most of the same parameters that can be applied to + * {@link android.widget.TextView} for displaying and formatting text. + * + * Optionally, a {@link Path} may be supplied on which to draw the text. + * + * A TextDrawable has an intrinsic size equal to that required to draw all + * the text it has been supplied, when possible. In cases where a {@link Path} + * has been supplied, the caller must explicitly call + * {@link #setBounds(Rect) setBounds()} to provide the Drawable + * size based on the Path constraints. + */ +public class TextDrawable extends Drawable { + + /* Platform XML constants for typeface */ + private static final int SANS = 1; + private static final int SERIF = 2; + private static final int MONOSPACE = 3; + + /* Resources for scaling values to the given device */ + private Resources mResources; + /* Paint to hold most drawing primitives for the text */ + private TextPaint mTextPaint; + /* Layout is used to measure and draw the text */ + private StaticLayout mTextLayout; + /* Alignment of the text inside its bounds */ + private Layout.Alignment mTextAlignment = Layout.Alignment.ALIGN_NORMAL; + /* Optional path on which to draw the text */ + private Path mTextPath; + /* Stateful text color list */ + private ColorStateList mTextColors; + /* Container for the bounds to be reported to widgets */ + private Rect mTextBounds; + /* Text string to draw */ + private CharSequence mText = ""; + private final Drawable mDrawable; + + /* Attribute lists to pull default values from the current theme */ + private static final int[] themeAttributes = { + android.R.attr.textAppearance + }; + private static final int[] appearanceAttributes = { + android.R.attr.textSize, + android.R.attr.typeface, + android.R.attr.textStyle, + android.R.attr.textColor + }; + private float mTextSizeFactor; + + public TextDrawable(Context context) { + this(context, null); + } + + public TextDrawable(Context context, Drawable drawable) { + super(); + mDrawable = drawable; + //Used to load and scale resource items + mResources = context.getResources(); + //Definition of this drawables size + mTextBounds = new Rect(); + //Paint to use for the text + mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.density = mResources.getDisplayMetrics().density; + mTextPaint.setDither(true); + + if (mDrawable != null) { + setBounds(mDrawable.getBounds()); + } + + int textSize = 15; + ColorStateList textColor = null; + int styleIndex = -1; + int typefaceIndex = -1; + + //Set default parameters from the current theme + TypedArray a = context.getTheme().obtainStyledAttributes(themeAttributes); + int appearanceId = a.getResourceId(0, -1); + a.recycle(); + + TypedArray ap = null; + if (appearanceId != -1) { + ap = context.obtainStyledAttributes(appearanceId, appearanceAttributes); + } + if (ap != null) { + for (int i=0; i < ap.getIndexCount(); i++) { + int attr = ap.getIndex(i); + switch (attr) { + case 0: //Text Size + textSize = a.getDimensionPixelSize(attr, textSize); + break; + case 1: //Typeface + typefaceIndex = a.getInt(attr, typefaceIndex); + break; + case 2: //Text Style + styleIndex = a.getInt(attr, styleIndex); + break; + case 3: //Text Color + textColor = a.getColorStateList(attr); + break; + default: + break; + } + } + + ap.recycle(); + } + + setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000)); + setRawTextSize(textSize); + + Typeface tf = null; + switch (typefaceIndex) { + case SANS: + tf = Typeface.SANS_SERIF; + break; + + case SERIF: + tf = Typeface.SERIF; + break; + + case MONOSPACE: + tf = Typeface.MONOSPACE; + break; + } + + setTypeface(tf, styleIndex); + } + + /** + * Set the text that will be displayed + * @param text Text to display + */ + public void setText(CharSequence text) { + if (text == null) text = ""; + + mText = text; + + measureContent(); + } + + /** + * Return the text currently being displayed + */ + public CharSequence getText() { + return mText; + } + + /** + * Return the current text size, in pixels + */ + public float getTextSize() { + return mTextPaint.getTextSize(); + } + + /** + * Set the text size. The value will be interpreted in "sp" units + * @param size Text size value, in sp + */ + public void setTextSize(float size) { + setTextSize(TypedValue.COMPLEX_UNIT_SP, size); + } + + /** + * Set the text size, using the supplied complex units + * @param unit Units for the text size, such as dp or sp + * @param size Text size value + */ + public void setTextSize(int unit, float size) { + float dimension = TypedValue.applyDimension(unit, size, + mResources.getDisplayMetrics()); + setRawTextSize(dimension); + } + + /** + * Text size compare to canvas size + */ + public void setTextSizeFactor(float factor) { + mTextSizeFactor = factor; + } + + /* + * Set the text size, in raw pixels + */ + private void setRawTextSize(float size) { + if (size != mTextPaint.getTextSize()) { + mTextPaint.setTextSize(size); + + measureContent(); + } + } + + /** + * Return the horizontal stretch factor of the text + */ + public float getTextScaleX() { + return mTextPaint.getTextScaleX(); + } + + /** + * Set the horizontal stretch factor of the text + * @param size Text scale factor + */ + public void setTextScaleX(float size) { + if (size != mTextPaint.getTextScaleX()) { + mTextPaint.setTextScaleX(size); + measureContent(); + } + } + + /** + * Return the current text alignment setting + */ + public Layout.Alignment getTextAlign() { + return mTextAlignment; + } + + /** + * Set the text alignment. The alignment itself is based on the text layout direction. + * For LTR text NORMAL is left aligned and OPPOSITE is right aligned. + * For RTL text, those alignments are reversed. + * @param align Text alignment value. Should be set to one of: + * + * {@link Layout.Alignment#ALIGN_NORMAL}, + * {@link Layout.Alignment#ALIGN_NORMAL}, + * {@link Layout.Alignment#ALIGN_OPPOSITE}. + */ + public void setTextAlign(Layout.Alignment align) { + if (mTextAlignment != align) { + mTextAlignment = align; + measureContent(); + } + } + + /** + * Sets the typeface and style in which the text should be displayed. + * Note that not all Typeface families actually have bold and italic + * variants, so you may need to use + * {@link #setTypeface(Typeface, int)} to get the appearance + * that you actually want. + */ + public void setTypeface(Typeface tf) { + if (mTextPaint.getTypeface() != tf) { + mTextPaint.setTypeface(tf); + + measureContent(); + } + } + + /** + * Sets the typeface and style in which the text should be displayed, + * and turns on the fake bold and italic bits in the Paint if the + * Typeface that you provided does not have all the bits in the + * style that you specified. + * + */ + public void setTypeface(Typeface tf, int style) { + if (style > 0) { + if (tf == null) { + tf = Typeface.defaultFromStyle(style); + } else { + tf = Typeface.create(tf, style); + } + + setTypeface(tf); + // now compute what (if any) algorithmic styling is needed + int typefaceStyle = tf != null ? tf.getStyle() : 0; + int need = style & ~typefaceStyle; + mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0); + mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0); + } else { + mTextPaint.setFakeBoldText(false); + mTextPaint.setTextSkewX(0); + setTypeface(tf); + } + } + + /** + * Return the current typeface and style that the Paint + * using for display. + */ + public Typeface getTypeface() { + return mTextPaint.getTypeface(); + } + + /** + * Set a single text color for all states + * @param color Color value such as {@link Color#WHITE} or {@link Color#argb(int, int, int, int)} + */ + public void setTextColor(int color) { + setTextColor(ColorStateList.valueOf(color)); + } + + /** + * Set the text color as a state list + * @param colorStateList ColorStateList of text colors, such as inflated from an R.color resource + */ + public void setTextColor(ColorStateList colorStateList) { + mTextColors = colorStateList; + updateTextColors(getState()); + } + + /** + * Optional Path object on which to draw the text. If this is set, + * TextDrawable cannot properly measure the bounds this drawable will need. + * You must call {@link #setBounds(int, int, int, int) setBounds()} before + * applying this TextDrawable to any View. + * + * Calling this method with null will remove any Path currently attached. + */ + public void setTextPath(Path path) { + if (mTextPath != path) { + mTextPath = path; + measureContent(); + } + } + + /** + * Internal method to take measurements of the current contents and apply + * the correct bounds when possible. + */ + private void measureContent() { + //If drawing to a path, we cannot measure intrinsic bounds + //We must resly on setBounds being called externally + if (mTextPath != null) { + //Clear any previous measurement + mTextLayout = null; + mTextBounds.setEmpty(); + } else { + //Measure text bounds + double desired = Math.ceil(Layout.getDesiredWidth(mText, mTextPaint)); + + if (mDrawable != null) { + desired = mDrawable.getIntrinsicWidth(); + } + + mTextLayout = new StaticLayout(mText, mTextPaint, (int) desired, + mTextAlignment, 1.0f, 0.0f, false); + + mTextBounds.set(0, 0, mTextLayout.getWidth(), mTextLayout.getHeight()); + } + + //We may need to be redrawn + invalidateSelf(); + } + + /** + * Internal method to apply the correct text color based on the drawable's state + */ + private boolean updateTextColors(int[] stateSet) { + int newColor = mTextColors.getColorForState(stateSet, Color.WHITE); + if (mTextPaint.getColor() != newColor) { + mTextPaint.setColor(newColor); + + // fully transparent text + mTextPaint.setAlpha(1); + mTextPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)); + + return true; + } + + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + //Update the internal bounds in response to any external requests + mTextBounds.set(bounds); + } + + @Override + public boolean isStateful() { + /* + * The drawable's ability to represent state is based on + * the text color list set + */ + return mTextColors.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + //Upon state changes, grab the correct text color + return updateTextColors(state); + } + + @Override + public int getIntrinsicHeight() { + //Return the vertical bounds measured, or -1 if none + if (mTextBounds.isEmpty()) { + return -1; + } else { + return (mTextBounds.bottom - mTextBounds.top); + } + } + + @Override + public int getIntrinsicWidth() { + //Return the horizontal bounds measured, or -1 if none + if (mTextBounds.isEmpty()) { + return -1; + } else { + return (mTextBounds.right - mTextBounds.left); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + final Rect bounds = getBounds(); + + final int count = canvas.save(); + //canvas.translate(bounds.left, bounds.top); + + if (mDrawable != null) { + // scale drawable to fit canvas + Rect clipBounds = canvas.getClipBounds(); + mDrawable.setBounds(clipBounds); + + mDrawable.draw(canvas); + } + + if (mTextPath == null) { + //Allow the layout to draw the text + + // Center text vertically!! + canvas.translate((bounds.width() / 2f) - (mTextLayout.getWidth() / 2f), (bounds.height() / 2f) - ((mTextLayout.getHeight() / 2f))); + + if (mTextSizeFactor > 0) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, bounds.height() * mTextSizeFactor); + } + + mTextLayout.draw(canvas); + + // Set text transparent + //canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); + } else { + //Draw directly on the canvas using the supplied path + canvas.drawTextOnPath(mText.toString(), mTextPath, 0, 0, mTextPaint); + } + canvas.restoreToCount(count); + } + + @Override + public void setAlpha(int alpha) { + if (mTextPaint.getAlpha() != alpha) { + mTextPaint.setAlpha(alpha); + } + } + + @Override + public int getOpacity() { + return mTextPaint.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + if (mTextPaint.getColorFilter() != cf) { + mTextPaint.setColorFilter(cf); + } + } + +} diff --git a/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/widgets/DialogTitle.java b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/widgets/DialogTitle.java new file mode 100644 index 0000000..708c3dd --- /dev/null +++ b/leankeykeyboard/src/main/java/com/liskovsoft/leankeyboard/widgets/DialogTitle.java @@ -0,0 +1,64 @@ +// Useful links: +// https://android.googlesource.com/platform/frameworks/base/+/de47f1c358c8186ff3e14b887d5869f69b9a9d6c/core/java/com/android/internal/widget/DialogTitle.java +// com.android.internal.widget.DialogTitle: https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/layout/alert_dialog.xml +// https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/com/android/internal/app/AlertController.java +// : https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/res/values/attrs.xml + + +package com.liskovsoft.leankeyboard.widgets; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.Layout; +import android.util.AttributeSet; +import android.util.TypedValue; +import androidx.appcompat.widget.AppCompatTextView; +import com.liskovsoft.leankeykeyboard.R; + +/** + * Used by dialogs to change the font size and number of lines to try to fit + * the text to the available space. + */ +public class DialogTitle extends AppCompatTextView { + + public DialogTitle(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + public DialogTitle(Context context, AttributeSet attrs) { + super(context, attrs); + } + public DialogTitle(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final Layout layout = getLayout(); + if (layout != null) { + final int lineCount = layout.getLineCount(); + if (lineCount > 0) { + final int ellipsisCount = layout.getEllipsisCount(lineCount - 1); + if (ellipsisCount > 0) { + setSingleLine(false); + + TypedArray a = getContext().obtainStyledAttributes(null, + R.styleable.TextAppearance, + android.R.attr.textAppearanceMedium, + android.R.style.TextAppearance_Medium); + final int textSize = a.getDimensionPixelSize( + R.styleable.TextAppearance_textSize, + (int) (20 * getResources().getDisplayMetrics().density)); + final int textColor = a.getColor( + R.styleable.TextAppearance_textColor, 0xffffffff); + // textSize is already expressed in pixels + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + setTextColor(textColor); + setMaxLines(2); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + } + } +} diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_available.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_available.png new file mode 100644 index 0000000..08517dd Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_available.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_focus.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_focus.png new file mode 100644 index 0000000..400d9d7 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_focus.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_off.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_off.png new file mode 100644 index 0000000..9d03a14 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_off.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_recording.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_recording.png new file mode 100644 index 0000000..a21b987 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_recording.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/key_selector.9.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/key_selector.9.png new file mode 100644 index 0000000..7a3c25e Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/key_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/touch_selector.9.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/touch_selector.9.png new file mode 100644 index 0000000..fe2bdb2 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/touch_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/vs_reactive_dark.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/vs_reactive_dark.png new file mode 100644 index 0000000..7bebe92 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/vs_reactive_dark.png differ diff --git a/leankeykeyboard/src/main/res/drawable-hdpi-v4/vs_reactive_light.png b/leankeykeyboard/src/main/res/drawable-hdpi-v4/vs_reactive_light.png new file mode 100644 index 0000000..bed1747 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-hdpi-v4/vs_reactive_light.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_available.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_available.png new file mode 100644 index 0000000..87011c9 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_available.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_focus.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_focus.png new file mode 100644 index 0000000..60f7a0c Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_focus.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_off.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_off.png new file mode 100644 index 0000000..ad8de62 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_off.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_recording.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_recording.png new file mode 100644 index 0000000..f163319 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_recording.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/key_selector.9.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/key_selector.9.png new file mode 100644 index 0000000..1a17e00 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/key_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/touch_selector.9.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/touch_selector.9.png new file mode 100644 index 0000000..ed4ba36 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/touch_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/vs_reactive_dark.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/vs_reactive_dark.png new file mode 100644 index 0000000..f30f7ae Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/vs_reactive_dark.png differ diff --git a/leankeykeyboard/src/main/res/drawable-mdpi-v4/vs_reactive_light.png b/leankeykeyboard/src/main/res/drawable-mdpi-v4/vs_reactive_light.png new file mode 100644 index 0000000..fac1240 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-mdpi-v4/vs_reactive_light.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_accent_close.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_accent_close.png new file mode 100644 index 0000000..8526a43 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_accent_close.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_alphabet.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_alphabet.png new file mode 100644 index 0000000..16f71a0 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_alphabet.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_clipboard.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_clipboard.png new file mode 100644 index 0000000..359bb45 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_clipboard.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_delete.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_delete.png new file mode 100644 index 0000000..2704ab0 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_delete.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_left_arrow.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_left_arrow.png new file mode 100644 index 0000000..395a640 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_left_arrow.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_right_arrow.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_right_arrow.png new file mode 100644 index 0000000..a750f2e Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_right_arrow.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on.png new file mode 100644 index 0000000..bd6385b Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark.png new file mode 100644 index 0000000..68ff338 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark2.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark2.png new file mode 100644 index 0000000..bd6385b Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark2.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark3.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark3.png new file mode 100644 index 0000000..139c9cf Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_dark3.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_white.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_white.png new file mode 100644 index 0000000..bd6385b Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_lock_on_white.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_off.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_off.png new file mode 100644 index 0000000..db5bf4e Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_off.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_on.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_on.png new file mode 100644 index 0000000..f128b13 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_shift_on.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_space.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_space.png new file mode 100644 index 0000000..925b7ac Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_space.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_symbols.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_symbols.png new file mode 100644 index 0000000..e8315a5 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_symbols.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_voice.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_voice.png new file mode 100644 index 0000000..cd539a7 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_voice.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_world.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_world.png new file mode 100644 index 0000000..623c490 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_ime_world.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_launcher.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_launcher.png new file mode 100644 index 0000000..1929ea4 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/ic_launcher.png differ diff --git a/leankeykeyboard/src/main/res/drawable-nodpi-v4/key_selector_square.png b/leankeykeyboard/src/main/res/drawable-nodpi-v4/key_selector_square.png new file mode 100644 index 0000000..59c8e48 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-nodpi-v4/key_selector_square.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_available.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_available.png new file mode 100644 index 0000000..bb38817 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_available.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_focus.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_focus.png new file mode 100644 index 0000000..721152c Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_focus.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_off.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_off.png new file mode 100644 index 0000000..c0752ea Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_off.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_recording.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_recording.png new file mode 100644 index 0000000..34b1167 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/ic_voice_recording.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/key_selector.9.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/key_selector.9.png new file mode 100644 index 0000000..921f00b Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/key_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/touch_selector.9.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/touch_selector.9.png new file mode 100644 index 0000000..013c8a5 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/touch_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/vs_reactive_dark.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/vs_reactive_dark.png new file mode 100644 index 0000000..e55967c Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/vs_reactive_dark.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xhdpi-v4/vs_reactive_light.png b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/vs_reactive_light.png new file mode 100644 index 0000000..16d4a76 Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xhdpi-v4/vs_reactive_light.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xxhdpi-v4/key_selector.9.png b/leankeykeyboard/src/main/res/drawable-xxhdpi-v4/key_selector.9.png new file mode 100644 index 0000000..f6613ed Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xxhdpi-v4/key_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable-xxhdpi-v4/touch_selector.9.png b/leankeykeyboard/src/main/res/drawable-xxhdpi-v4/touch_selector.9.png new file mode 100644 index 0000000..d0a06cf Binary files /dev/null and b/leankeykeyboard/src/main/res/drawable-xxhdpi-v4/touch_selector.9.png differ diff --git a/leankeykeyboard/src/main/res/drawable/selector_caps_shift.xml b/leankeykeyboard/src/main/res/drawable/selector_caps_shift.xml new file mode 100644 index 0000000..6215829 --- /dev/null +++ b/leankeykeyboard/src/main/res/drawable/selector_caps_shift.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/leankeykeyboard/src/main/res/drawable/vs_micbtn_off_selector.xml b/leankeykeyboard/src/main/res/drawable/vs_micbtn_off_selector.xml new file mode 100644 index 0000000..dcae5d2 --- /dev/null +++ b/leankeykeyboard/src/main/res/drawable/vs_micbtn_off_selector.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/leankeykeyboard/src/main/res/drawable/vs_micbtn_on_selector.xml b/leankeykeyboard/src/main/res/drawable/vs_micbtn_on_selector.xml new file mode 100644 index 0000000..dcae5d2 --- /dev/null +++ b/leankeykeyboard/src/main/res/drawable/vs_micbtn_on_selector.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/leankeykeyboard/src/main/res/drawable/vs_micbtn_rec_selector.xml b/leankeykeyboard/src/main/res/drawable/vs_micbtn_rec_selector.xml new file mode 100644 index 0000000..f583d88 --- /dev/null +++ b/leankeykeyboard/src/main/res/drawable/vs_micbtn_rec_selector.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/leankeykeyboard/src/main/res/layout/activity_kb_layout.xml b/leankeykeyboard/src/main/res/layout/activity_kb_layout.xml new file mode 100644 index 0000000..e5b1b02 --- /dev/null +++ b/leankeykeyboard/src/main/res/layout/activity_kb_layout.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/leankeykeyboard/src/main/res/layout/candidate.xml b/leankeykeyboard/src/main/res/layout/candidate.xml new file mode 100644 index 0000000..8f78be5 --- /dev/null +++ b/leankeykeyboard/src/main/res/layout/candidate.xml @@ -0,0 +1,5 @@ + + +