Initial commit (recreated history, MIT license)

This commit is contained in:
Yuriy Liskov
2026-02-12 06:30:30 +02:00
commit 79c310d9b1
305 changed files with 16686 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View 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
View File

@@ -0,0 +1,66 @@
![Logo of LeanKeyboard](img/leankeykeyboard_logo_small.png "Logo of LeanKeyboard") LeanKeyboard
=========
[![MPLv2 License](http://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](https://www.mozilla.org/MPL/2.0/)
__LeanKeyboard: Keyboard for Android-based set-top boxes and TVs:__
* <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:
![Screenshot of LeanKeyboard](img/leankeykeyboard_screenshot_01.png "Screenshot of LeanKeyboard")
![Screenshot of LeanKeyboard](img/leankeykeyboard_screenshot_02.png "Screenshot of LeanKeyboard")
![Screenshot of LeanKeyboard](img/leankeykeyboard_screenshot_03.png "Screenshot of LeanKeyboard")

78
build.gradle Normal file
View 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
View File

@@ -0,0 +1,2 @@
android.useAndroidX=true
android.enableJetifier=true

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file
View File

Binary file not shown.

View 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
View 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
View 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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
img/screen4.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
img/screen5.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

3
leankeykeyboard/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/build
/playstore
/origin

View 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
View 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

View File

@@ -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());
}
}

View 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>

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,4 @@
package com.liskovsoft.leankeyboard.activity.settings;
public class KbSettingsActivity2 extends KbSettingsActivity {
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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) {
}
}

View File

@@ -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) {
;
}
}
}

View File

@@ -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, "&", "&amp;");
str = replaceString(str, "<", "&lt;");
str = replaceString(str, ">", "&gt;");
str = replaceString(str, "\"", "&quot;");
str = replaceString(str, "'", "&apos;");
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());
// }
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package com.liskovsoft.leankeyboard.addons.voice;
import android.content.Intent;
interface ActivityListener {
void onActivityResult(int requestCode, int resultCode, Intent data);
}

View File

@@ -0,0 +1,5 @@
package com.liskovsoft.leankeyboard.addons.voice;
public interface RecognizerCallback {
void openSearchPage(String searchText);
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,5 @@
package com.liskovsoft.leankeyboard.addons.voice;
interface SearchCallback {
void openSearchPage(String searchText);
}

View File

@@ -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));
}
}
}
}

View File

@@ -0,0 +1,5 @@
package com.liskovsoft.leankeyboard.addons.voice;
interface VoiceDialog {
boolean displaySpeechRecognizer();
}

View File

@@ -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]);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

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