Initial commit (recreated history, MIT license)
24
.gitignore
vendored
Normal file
@@ -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
|
||||
22
LICENSE
Normal file
@@ -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.
|
||||
66
README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
 LeanKeyboard
|
||||
=========
|
||||
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
__LeanKeyboard: Keyboard for Android-based set-top boxes and TVs:__
|
||||
|
||||
* <a href="https://play.google.com/store/apps/details?id=org.liskovsoft.androidtv.rukeyboard" target="_blank">Google Play page</a>
|
||||
* <a href="https://t.me/LeanKeyboard">Telegram group</a>
|
||||
|
||||
### 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__
|
||||
* <a href="https://github.com/yuliskov/LeanKeyboard/wiki/How-to-Install-LeanKeyKeyboard-on-FireTV">Install LeanKeyKeyboard (only FireTV needed)</a>
|
||||
|
||||
__Standard installation via ADB__
|
||||
* If you don't know how to sideload/install apps via ADB, read a tutorial (e.g. <a href="http://kodi.wiki/view/HOW-TO:Install_Kodi_on_Fire_TV" target="_blank">this one</a>)
|
||||
* <a href="https://github.com/yuliskov/LeanKeyboard/releases" target="_blank">Download latest LeanKeyKeyboard APK</a> 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 :)
|
||||
<!-- * [QIWI (RU, Visa)](https://qiwi.com/n/GUESS025) -->
|
||||
<!-- * [DonatePay (RU, **PayPal**, Visa)](https://new.donatepay.ru/@459197) -->
|
||||
* [**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:
|
||||

|
||||

|
||||

|
||||
78
build.gradle
Normal file
@@ -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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
2
gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -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
|
||||
160
gradlew
vendored
Normal file
@@ -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 "$@"
|
||||
90
gradlew.bat
vendored
Normal file
@@ -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
|
||||
BIN
img/leankeykeyboard_logo_small.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
img/leankeykeyboard_screenshot_01.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
img/leankeykeyboard_screenshot_02.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
img/leankeykeyboard_screenshot_03.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
img/screen4.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
img/screen5.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
3
leankeykeyboard/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/build
|
||||
/playstore
|
||||
/origin
|
||||
86
leankeykeyboard/build.gradle
Normal file
@@ -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
|
||||
}
|
||||
25
leankeykeyboard/proguard-rules.pro
vendored
Normal file
@@ -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
|
||||
@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
}
|
||||
130
leankeykeyboard/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.liskovsoft.leankeykeyboard">
|
||||
|
||||
<!-- Use older sdk -->
|
||||
<uses-sdk tools:overrideLibrary="
|
||||
android.support.v17.leanback,
|
||||
androidx.leanback,
|
||||
com.algolia.instantsearch.voice"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.microphone"
|
||||
android:required="false"/>
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
|
||||
<uses-permission android:name="com.amazon.tv.ime.permission.KEYBOARD_STATE"/>
|
||||
<!-- Google Play Store fix -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:banner="@mipmap/ic_banner_main"
|
||||
android:icon="@mipmap/ic_launcher_main"
|
||||
android:label="@string/ime_name"
|
||||
android:theme="@style/Theme.Leanback">
|
||||
|
||||
<meta-data
|
||||
android:name="tv.reference_plus_preload_category"
|
||||
android:value="oem_1p_app" />
|
||||
|
||||
<activity
|
||||
android:name="com.liskovsoft.leankeyboard.activity.settings.KbSettingsActivity2"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.liskovsoft.leankeyboard.activity.settings.KbSettingsActivity"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.liskovsoft.leankeyboard.activity.settings.KbLayoutActivity"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.liskovsoft.leankeyboard.activity.PermissionsActivity"
|
||||
android:launchMode="singleTop">
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.liskovsoft.leankeyboard.addons.voice.RecognizerIntentActivity"
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:launchMode="singleTop"/>
|
||||
|
||||
<activity
|
||||
android:name="com.liskovsoft.leankeyboard.activity.settings.KbActivationActivity"
|
||||
android:launchMode="singleTop">
|
||||
<meta-data
|
||||
android:name="package"
|
||||
android:value="com.android.settings"/>
|
||||
<meta-data
|
||||
android:name="class"
|
||||
android:value="com.android.settings.Settings$LanguageAndInputSettingsActivity"/>
|
||||
<!-- "Languages & input" on old api -->
|
||||
<meta-data
|
||||
android:name="package_alt"
|
||||
android:value="com.android.settings"/>
|
||||
<meta-data
|
||||
android:name="class_alt"
|
||||
android:value="com.android.settings.Settings$InputMethodAndLanguageSettingsActivity"/>
|
||||
<!-- Last try (if above not found) -->
|
||||
<meta-data
|
||||
android:name="intent"
|
||||
android:value="android.settings.INPUT_METHOD_SETTINGS"/>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="com.liskovsoft.leankeyboard.ime.LeanbackImeService"
|
||||
android:label="@string/ime_service_name"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.view.im"
|
||||
android:resource="@xml/method"/>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name="com.liskovsoft.leankeyboard.receiver.RestartServiceReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED"/>
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED"/>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED"/>
|
||||
|
||||
<data android:scheme="package"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name="com.liskovsoft.leankeyboard.receiver.TextUpdateReceiver"
|
||||
android:permission="com.amazon.tv.ime.permission.KEYBOARD_STATE"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.amazon.tv.ime.action.CLIENT_TEXT"/>
|
||||
<action android:name="com.amazon.tv.ime.action.IME_TEXT"/>
|
||||
<action android:name="com.amazon.tv.ime.action.KEYBOARD_STATE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.liskovsoft.leankeyboard.activity.settings;
|
||||
|
||||
public class KbSettingsActivity2 extends KbSettingsActivity {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.liskovsoft.leankeyboard.addons.keyboards;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeyboardFactory {
|
||||
List<? extends KeyboardBuilder> getAllAvailableKeyboards(Context context);
|
||||
boolean needUpdate();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<? extends KeyboardBuilder> mKeyboardBuilders;
|
||||
private List<KeyboardData> 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<KeyboardData> buildAllKeyboards() {
|
||||
List<KeyboardData> 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<Context> 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<AddOnImpl> mAddOnWeakReference;
|
||||
private final SparseIntArray mAttributesMapping = new SparseIntArray();
|
||||
private final SparseArrayCompat<int[]> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<E extends AddOn> {
|
||||
|
||||
private static final class AddOnsComparator implements Comparator<AddOn> {
|
||||
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<AddOnsFactory<?>> 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<E> mAddOns = new ArrayList<>();
|
||||
private final HashMap<String, E> 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<E> 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<E> local = getAddOnsFromResId(askContext, askContext, mBuildInAddOnsResId);
|
||||
//for (E addon : local) {
|
||||
// Logger.d(TAG, "Local add-on %s loaded", addon.getId());
|
||||
//}
|
||||
//mAddOns.addAll(local);
|
||||
ArrayList<E> 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<E> newAddOns) {
|
||||
for (E addOn : newAddOns)
|
||||
mAddOnsById.put(addOn.getId(), addOn);
|
||||
}
|
||||
|
||||
private ArrayList<E> getExternalAddOns(Context askContext) {
|
||||
final ArrayList<E> externalAddOns = new ArrayList<>();
|
||||
|
||||
if (!mReadExternalPacksToo)//this will disable external packs (API careful stage)
|
||||
return externalAddOns;
|
||||
|
||||
final List<ResolveInfo> 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<E> 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<E> 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<E> 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<E> parseAddOnsFromXml(Context askContext, Context context, XmlPullParser xml) {
|
||||
final ArrayList<E> 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);
|
||||
}
|
||||
@@ -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<Integer> 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<remoteMappedStyleable.length; i++) {
|
||||
remoteMappedStyleable[i] = styleableIdList.get(i);
|
||||
}
|
||||
|
||||
return remoteMappedStyleable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.keyboards;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import android.inputmethodservice.Keyboard;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardBuilder;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons.AddOn;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons.AddOnImpl;
|
||||
import com.liskovsoft.leankeykeyboard.R;
|
||||
|
||||
public class ApkKeyboardAddOnAndBuilder extends AddOnImpl implements KeyboardBuilder {
|
||||
|
||||
public static final String KEYBOARD_PREF_PREFIX = "keyboard_";
|
||||
|
||||
private final int mResId;
|
||||
private final int mLandscapeResId;
|
||||
private final String mDefaultDictionary;
|
||||
private final int mQwertyTranslationId;
|
||||
private final String mAdditionalIsLetterExceptions;
|
||||
private final String mSentenceSeparators;
|
||||
private final boolean mKeyboardDefaultEnabled;
|
||||
|
||||
public ApkKeyboardAddOnAndBuilder(Context askContext, Context packageContext, String id, int nameResId,
|
||||
int layoutResId, int landscapeLayoutResId,
|
||||
String defaultDictionary, int iconResId,
|
||||
int physicalTranslationResId,
|
||||
String additionalIsLetterExceptions,
|
||||
String sentenceSeparators,
|
||||
String description,
|
||||
int keyboardIndex,
|
||||
boolean keyboardDefaultEnabled) {
|
||||
super(askContext, packageContext, KEYBOARD_PREF_PREFIX + id, nameResId, description, keyboardIndex);
|
||||
|
||||
mResId = layoutResId;
|
||||
if (landscapeLayoutResId == AddOn.INVALID_RES_ID) {
|
||||
mLandscapeResId = mResId;
|
||||
} else {
|
||||
mLandscapeResId = landscapeLayoutResId;
|
||||
}
|
||||
|
||||
mDefaultDictionary = defaultDictionary;
|
||||
//mIconResId = iconResId;
|
||||
mAdditionalIsLetterExceptions = additionalIsLetterExceptions;
|
||||
mSentenceSeparators = sentenceSeparators;
|
||||
mQwertyTranslationId = physicalTranslationResId;
|
||||
mKeyboardDefaultEnabled = keyboardDefaultEnabled;
|
||||
}
|
||||
|
||||
public boolean getKeyboardDefaultEnabled() {
|
||||
return mKeyboardDefaultEnabled;
|
||||
}
|
||||
|
||||
public String getKeyboardLocale() {
|
||||
return mDefaultDictionary;
|
||||
}
|
||||
|
||||
public String getSentenceSeparators() {
|
||||
return mSentenceSeparators;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public android.inputmethodservice.Keyboard createAbcKeyboard() {
|
||||
Context remoteContext = getPackageContext();
|
||||
if (remoteContext == null) return null;
|
||||
return new android.inputmethodservice.Keyboard(remoteContext, mLandscapeResId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keyboard createSymKeyboard() {
|
||||
return new Keyboard(getPackageContext(), R.xml.sym_en_us);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keyboard createNumKeyboard() {
|
||||
return new Keyboard(getPackageContext(), R.xml.number);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.keyboards;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.inputmethodservice.*;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardBuilder;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.KeyboardFactory;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons.AddOn;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.addons.AddOnsFactory;
|
||||
import com.liskovsoft.leankeyboard.addons.keyboards.extkeyboards.utils.log.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class ApkLangKeyboardFactory extends AddOnsFactory<ApkKeyboardAddOnAndBuilder> 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<? extends KeyboardBuilder> getAllAvailableKeyboards(Context context) {
|
||||
return getAllAddOns(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needUpdate() {
|
||||
// TODO: implement need update
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<ApkKeyboardAddOnAndBuilder> getEnabledKeyboards(Context askContext) {
|
||||
final List<ApkKeyboardAddOnAndBuilder> 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<ApkKeyboardAddOnAndBuilder> 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<? extends KeyboardBuilder> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<String> getAllLogLinesList() {
|
||||
ArrayList<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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().
|
||||
* <p/>
|
||||
* 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 <em>after</em> 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<String, Object> 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 <em>after</em> 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<Object> 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
|
||||
* <em>after</em> 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<Object> 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 <em>after</em> 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 <em>at</em> 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 <string>: " + parser.getName());
|
||||
} else if (eventType == XmlPullParser.TEXT) {
|
||||
value += parser.getText();
|
||||
} else if (eventType == XmlPullParser.START_TAG) {
|
||||
throw new XmlPullParserException("Unexpected start tag in <string>: " + parser.getName());
|
||||
}
|
||||
}
|
||||
throw new XmlPullParserException(
|
||||
"Unexpected end of document in <string>");
|
||||
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) {
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <a href="mailto:bayard@generationjava.com">Henri Yandell</a>
|
||||
* @author <a href="mailto:menny|AT| evendanan{dot} net">Menny Even Danan - just
|
||||
* added some features on Henri's initial version</a>
|
||||
* @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<String> 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<String>();
|
||||
this.attrs = new StringBuffer();
|
||||
if (addXmlPrefix)
|
||||
this.writer.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\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("</");
|
||||
this.writer.write(name);
|
||||
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());
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.liskovsoft.leankeyboard.addons.keyboards.intkeyboards;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CheckedSource {
|
||||
List<CheckedItem> getItems();
|
||||
|
||||
interface CheckedItem {
|
||||
long getId();
|
||||
String getTitle();
|
||||
void onClick(boolean checked);
|
||||
boolean getChecked();
|
||||
}
|
||||
}
|
||||
@@ -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<KeyboardInfo> mInfos;
|
||||
|
||||
public KeyboardInfoAdapter(Context context) {
|
||||
mContext = context;
|
||||
mInfos = ResKeyboardInfo.getAllKeyboardInfos(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CheckedItem> getItems() {
|
||||
List<CheckedItem> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Drawable> mCachedSpace;
|
||||
|
||||
public ResKeyboardFactory(Context ctx) {
|
||||
mContext = ctx;
|
||||
mCachedSpace = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends KeyboardBuilder> getAllAvailableKeyboards(Context context) {
|
||||
List<KeyboardBuilder> result = new ArrayList<>();
|
||||
List<KeyboardInfo> 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<KeyboardInfo> 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<Key> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<KeyboardInfo> getAllKeyboardInfos(Context ctx) {
|
||||
List<KeyboardInfo> 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<KeyboardInfo> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Key> getKeys() {
|
||||
return mKeyboard.getKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Key> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Key> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.liskovsoft.leankeyboard.addons.voice;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
interface ActivityListener {
|
||||
void onActivityResult(int requestCode, int resultCode, Intent data);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.liskovsoft.leankeyboard.addons.voice;
|
||||
|
||||
public interface RecognizerCallback {
|
||||
void openSearchPage(String searchText);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.liskovsoft.leankeyboard.addons.voice;
|
||||
|
||||
interface SearchCallback {
|
||||
void openSearchPage(String searchText);
|
||||
}
|
||||
@@ -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<String> results = data.getStringArrayListExtra(
|
||||
RecognizerIntent.EXTRA_RESULTS);
|
||||
if (results != null && results.size() > 0) {
|
||||
mCallback.openSearchPage(results.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.liskovsoft.leankeyboard.addons.voice;
|
||||
|
||||
interface VoiceDialog {
|
||||
boolean displaySpeechRecognizer();
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<VoiceDialog> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Long, CheckedAction> mCheckedActions = new LinkedHashMap<>();
|
||||
private Map<Long, NextAction> 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<GuidedAction> 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<GuidedAction> 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<GuidedAction> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<ResolveInfo> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.<br/>
|
||||
* 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.<br/>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<br/>
|
||||
* If the app does not has permission then the user will be prompted to grant permissions<br/>
|
||||
* Required for the {@link Context#getExternalCacheDir()}<br/>
|
||||
* NOTE: runs async<br/>
|
||||
*
|
||||
* @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<br/>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<br/>
|
||||
* <a href="https://stackoverflow.com/questions/7449283/is-it-possible-to-have-both-physical-keyboard-and-soft-keyboard-active-at-the-sa">More info</a>
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<KeyChange> 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<String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.<br/>
|
||||
* Any manipulations with keys should be done here.
|
||||
*/
|
||||
private void setKeys(List<Key> keys) {
|
||||
mKeys = new KeyHolder[keys.size()];
|
||||
Iterator<Key> 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
|
||||
* <br/>
|
||||
* 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<Key> 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;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<TouchNavMotionTracker> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<RecognizerView.SavedState> CREATOR = new Creator<RecognizerView.SavedState>() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> getListInstalledPackages() {
|
||||
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
|
||||
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
List<ResolveInfo> pkgAppsList = mContext.getPackageManager().queryIntentActivities( mainIntent, 0);
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, Map<String, String>> scriptsByLocale = new HashMap<String, Map<String, String>>();
|
||||
|
||||
public static Map<String, String> getScriptsMap(String... keyValuePairs) {
|
||||
Map<String, String> languages = new HashMap<String, String>();
|
||||
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<String, String> scripts = scriptsByLocale.get(language);
|
||||
String script = scripts.get(country);
|
||||
return script == null ? scripts.get("") : script;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://stackoverflow.com/questions/40221711/android-context-getresources-updateconfiguration-deprecated/40704077#40704077">Modern Solution</a>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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 <code>null</code> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
// <declare-styleable name="TextAppearance">: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
BIN
leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_focus.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
leankeykeyboard/src/main/res/drawable-hdpi-v4/ic_voice_off.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
BIN
leankeykeyboard/src/main/res/drawable-hdpi-v4/key_selector.9.png
Normal file
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
BIN
leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_focus.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
leankeykeyboard/src/main/res/drawable-mdpi-v4/ic_voice_off.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |