commit a677cc3f262af2b33bad882fb9efcf4add783141 Author: Abderrahim-Mouhcine Date: Sun Aug 16 19:57:54 2026 +0100 Initial upload: clavier-ando-mibox diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6c003a --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Android / Gradle +build/ +.gradle/ +.kotlin/ +local.properties +.idea/ +*.iml +captures/ +.cxx/ +# Flutter / Dart +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +# Python / Node +__pycache__/ +*.pyc +node_modules/ +dist/ +.env +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/Commande d installation.txt b/Commande d installation.txt new file mode 100644 index 0000000..d386c14 --- /dev/null +++ b/Commande d installation.txt @@ -0,0 +1,16 @@ +cd "D:\07-Logiciels\18-ClavierAndoMiBox" + +.\gradlew.bat clean :tv_keyboard:assembleDebug +adb -s 192.168.1.8:5555 install -r .\tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk + +adb -s 192.168.1.8:5555 shell monkey -p com.ando.miboxkeyboard.tv 1 + +adb -s 192.168.1.8:5555 shell ime enable com.ando.miboxkeyboard.tv/.RemoteKeyboardImeService +adb -s 192.168.1.8:5555 shell ime set com.ando.miboxkeyboard.tv/.RemoteKeyboardImeService + +Pour tester : + +curl.exe http://192.168.1.8:8765/ping +curl.exe http://192.168.1.8:8765/picker + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5f2dff --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# ClavierAndoMiBox V1 + +Projet Android contenant deux applications : + +- `tv_keyboard` : application Mi Box / Google TV. Elle installe un clavier Android personnalisé et lance un mini serveur HTTP local sur le port `8765`. +- `phone_remote` : application smartphone Android. Elle envoie le texte vers la Mi Box par Wi-Fi local. + +## Chemin recommandé sur ton PC + +Copier le dossier extrait ici : + +```text +D:\07-Logiciels\18-ClavierAndoMiBox +``` + +## Construction dans Android Studio + +1. Ouvrir Android Studio. +2. `File > Open`. +3. Choisir : + +```text +D:\07-Logiciels\18-ClavierAndoMiBox +``` + +4. Attendre la synchronisation Gradle. +5. Construire les APKs : + +```bash +./gradlew :tv_keyboard:assembleDebug +./gradlew :phone_remote:assembleDebug +``` + +Sous Windows : + +```bat +gradlew.bat :tv_keyboard:assembleDebug +gradlew.bat :phone_remote:assembleDebug +``` + +Si le wrapper Gradle n'est pas créé, utiliser directement Android Studio : + +```text +Build > Build Bundle(s) / APK(s) > Build APK(s) +``` + +## APKs générés + +```text +tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk +phone_remote\build\outputs\apk\debug\phone_remote-debug.apk +``` + +## Installation Mi Box + +Depuis le PC : + +```bash +adb connect 192.168.1.56:5555 +adb install -r tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk +``` + +Puis sur la Mi Box : + +```text +Paramètres > Système > Clavier > Gérer les claviers +``` + +Activer : + +```text +Clavier Ando MiBox +``` + +Ensuite, dans une zone de texte, choisir ce clavier comme clavier actif. + +## Installation smartphone + +Installer : + +```text +phone_remote-debug.apk +``` + +Dans l'application smartphone : + +```text +IP : 192.168.1.56 +Port : 8765 +``` + +Puis écrire un texte et appuyer sur : + +```text +Envoyer le texte +``` + +## Test rapide depuis PC + +Quand le clavier Mi Box est actif dans une zone texte : + +```bash +curl -X POST http://192.168.1.56:8765/type -d "bonjour" +curl -X POST http://192.168.1.56:8765/enter +curl -X POST http://192.168.1.56:8765/backspace +curl -X POST http://192.168.1.56:8765/ping +``` + +## Limite V1 + +Le texte ne s'injecte que si : + +1. le clavier `Clavier Ando MiBox` est actif ; +2. une zone de texte est ouverte ; +3. le téléphone et la Mi Box sont sur le même Wi-Fi ; +4. l'IP de la Mi Box est correcte. + +Android ne permet pas à une app ordinaire d'écrire dans les champs texte d'une autre app. D'où le clavier personnalisé, merveilleuse petite acrobatie imposée par le système. diff --git a/README_V1.1_CLAVIER.md b/README_V1.1_CLAVIER.md new file mode 100644 index 0000000..f941ef3 --- /dev/null +++ b/README_V1.1_CLAVIER.md @@ -0,0 +1,17 @@ +# Clavier Ando MiBox V1.1 + +Ajouts : + +- Endpoint TV `/picker` : ouvre le sélecteur de clavier Android sur la Mi Box. +- Endpoint TV `/settings` : ouvre les paramètres clavier Android. +- App smartphone : boutons `Changer clavier Mi Box` et `Paramètres clavier`. +- IP par défaut de l’app smartphone : `192.168.1.8`. + +Tests PC : + +```powershell +curl.exe -X POST http://192.168.1.8:8765/picker +curl.exe -X POST http://192.168.1.8:8765/settings +``` + +Limite Android : une app normale ne peut pas sélectionner silencieusement un clavier à la place de l’utilisateur. Le bouton ouvre donc le menu officiel. diff --git a/README_V1.2.1_PICKER_RESPONSE.md b/README_V1.2.1_PICKER_RESPONSE.md new file mode 100644 index 0000000..c64be2a --- /dev/null +++ b/README_V1.2.1_PICKER_RESPONSE.md @@ -0,0 +1,23 @@ +# V1.2.1 - Correction picker depuis smartphone + +Corrections : +- Le serveur Mi Box répond à /picker avant d'ouvrir le sélecteur clavier. +- /picker et /settings supportent mieux GET/POST/OPTIONS. +- L'application smartphone appelle /picker et /settings en GET. +- L'application smartphone affiche la réponse exacte du serveur. +- IP par défaut : 192.168.1.8. + +Installation : +1. Extraire ce patch dans D:\07-Logiciels\18-ClavierAndoMiBox +2. Compiler et installer Mi Box : + .\gradlew.bat clean :tv_keyboard:assembleDebug + adb -s 192.168.1.8:5555 install -r .\tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk + adb -s 192.168.1.8:5555 shell monkey -p com.ando.miboxkeyboard.tv 1 +3. Compiler et installer smartphone : + .\gradlew.bat :phone_remote:assembleDebug + adb -s f10778d8 install -r .\phone_remote\build\outputs\apk\debug\phone_remote-debug.apk + +Tests : +- Chrome smartphone : http://192.168.1.8:8765/ping +- Chrome smartphone : http://192.168.1.8:8765/picker +- App smartphone : bouton Changer clavier Mi Box diff --git a/README_V1.2_SERVEUR_PERMANENT.md b/README_V1.2_SERVEUR_PERMANENT.md new file mode 100644 index 0000000..3344f86 --- /dev/null +++ b/README_V1.2_SERVEUR_PERMANENT.md @@ -0,0 +1,39 @@ +# V1.2 - Serveur permanent Mi Box + +Objectif : garder le serveur HTTP actif même quand Gboard est le clavier actif. + +Nouveaux composants côté Mi Box : + +- `AndoServerService` : service Android séparé du clavier IME. +- `RemoteCommandServer` : serveur HTTP unique sur le port 8765. +- `BootReceiver` : tente de relancer le service après redémarrage. + +Endpoints : + +- `GET /ping` : vérifie le serveur permanent. +- `POST /picker` : ouvre le sélecteur de clavier, même depuis Gboard si le service tourne. +- `POST /settings` : ouvre les paramètres clavier. +- `POST /type` : écrit du texte seulement si Clavier Ando MiBox est le clavier actif. +- `POST /enter` : Entrée seulement si Clavier Ando MiBox est actif. +- `POST /backspace` : efface un caractère seulement si Clavier Ando MiBox est actif. + +Installation : + +```powershell +cd "D:\07-Logiciels\18-ClavierAndoMiBox" + +.\gradlew.bat clean :tv_keyboard:assembleDebug + +adb -s 192.168.1.8:5555 install -r .\tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk + +adb -s 192.168.1.8:5555 shell monkey -p com.ando.miboxkeyboard.tv 1 +``` + +Après ouverture de l'application Mi Box une fois, le serveur doit répondre : + +```powershell +curl.exe http://192.168.1.8:8765/ping +curl.exe -X POST http://192.168.1.8:8765/picker +``` + +Note : Android peut tuer le service si la box manque de mémoire. Dans ce cas, ouvrir l'application "Clavier Ando MiBox" sur la Mi Box relance le serveur. diff --git a/README_V1.3.1_PHONE_PICKER_POST.md b/README_V1.3.1_PHONE_PICKER_POST.md new file mode 100644 index 0000000..9a68020 --- /dev/null +++ b/README_V1.3.1_PHONE_PICKER_POST.md @@ -0,0 +1,22 @@ +# V1.3.1 - Correction smartphone picker + +Ce patch modifie uniquement l'application smartphone. + +Changements : +- le bouton "Changer clavier Mi Box" appelle désormais `/picker` en POST, comme la commande PC validée `curl.exe -X POST .../picker` ; +- l'application affiche l'URL appelée dans le statut ; +- logs Android ajoutés sous le tag `AndoPhone`. + +Installation : +```powershell +cd "D:\07-Logiciels\18-ClavierAndoMiBox" +.\gradlew.bat clean :phone_remote:assembleDebug +adb -s f10778d8 uninstall com.ando.miboxkeyboard.phone +adb -s f10778d8 install -r .\phone_remote\build\outputs\apk\debug\phone_remote-debug.apk +``` + +Logs utiles : +```powershell +adb -s f10778d8 logcat -c +adb -s f10778d8 logcat | findstr /i "AndoPhone" +``` diff --git a/README_V1.3_FOREGROUND_SERVICE.md b/README_V1.3_FOREGROUND_SERVICE.md new file mode 100644 index 0000000..7b06a46 --- /dev/null +++ b/README_V1.3_FOREGROUND_SERVICE.md @@ -0,0 +1,33 @@ +# Clavier Ando MiBox - Patch V1.3 + +## Objectif + +Rendre le serveur HTTP côté Mi Box réellement persistant avec un Foreground Service. + +## Correction + +- `AndoServerService` passe en service premier plan. +- Notification persistante : `Clavier Ando MiBox`. +- `/picker` reste disponible même quand Gboard est actif et quand une autre app est ouverte. +- `SetupActivity` et `RemoteKeyboardImeService` relancent le serveur proprement. +- `BootReceiver` tente de relancer le serveur au démarrage. + +## Commandes + +```powershell +cd "D:\07-Logiciels\18-ClavierAndoMiBox" + +.\gradlew.bat clean :tv_keyboard:assembleDebug + +adb -s 192.168.1.8:5555 install -r .\tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk + +adb -s 192.168.1.8:5555 shell monkey -p com.ando.miboxkeyboard.tv 1 + +curl.exe http://192.168.1.8:8765/ping +curl.exe http://192.168.1.8:8765/picker +``` + +## Notes + +- `/picker` peut fonctionner avec Gboard actif. +- `/type`, `/enter`, `/backspace` nécessitent toujours que `Clavier Ando MiBox` soit le clavier actif. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5b7ccba --- /dev/null +++ b/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'com.android.application' version '8.7.3' apply false +} diff --git a/build_apks_windows.bat b/build_apks_windows.bat new file mode 100644 index 0000000..8a9a8b6 --- /dev/null +++ b/build_apks_windows.bat @@ -0,0 +1,5 @@ +@echo off +cd /d %~dp0 +gradlew.bat :tv_keyboard:assembleDebug +gradlew.bat :phone_remote:assembleDebug +pause diff --git a/env.md b/env.md new file mode 100644 index 0000000..1ac84a2 --- /dev/null +++ b/env.md @@ -0,0 +1,41 @@ +$env:JAVA_HOME = "D:\Program Files\Android\Android Studio\jbr" +$env:Path = "$env:JAVA_HOME\bin;$env:Path" + +java -version + +$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk" +$env:ANDROID_SDK_ROOT = "$env:LOCALAPPDATA\Android\Sdk" +$env:Path = "$env:ANDROID_HOME\platform-tools;$env:ANDROID_HOME\cmdline-tools\latest\bin;$env:ANDROID_HOME\emulator;$env:Path" + +adb version +adb devices + +$env:Path = "D:\flutter\bin;$env:Path" + +flutter --version +flutter doctor + +Get-ChildItem -Path "C:\","D:\" -Filter flutter.bat -Recurse -ErrorAction SilentlyContinue + +ajoute le chemin trouvé dans : $env:Path = "D:\quelquechose\flutter\bin;$env:Path" + +Set-Content -Path ".\local.properties" -Value "sdk.dir=$($env:LOCALAPPDATA.Replace('\','/'))/Android/Sdk" + +Get-Content .\local.properties + +$old = "D:\07-Logiciels\12-IPTV Andoid TV HERMES" +$new = "D:\07-Logiciels\18-ClavierAndoMiBox" + +Copy-Item "$old\gradlew.bat" "$new\" -Force +Copy-Item "$old\gradlew" "$new\" -Force -ErrorAction SilentlyContinue +Copy-Item "$old\gradle" "$new\gradle" -Recurse -Force + +adb devices + +adb connect 192.168.1.8:5555 +adb devices + +adb -s 192.168.1.8:5555 install -r .\tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk + +adb -s f10778d8 install -r .\phone_remote\build\outputs\apk\debug\phone_remote-debug.apk + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..667b332 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=false +android.nonTransitiveRClass=true +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2e11132 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@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 + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/install_tv_windows.bat b/install_tv_windows.bat new file mode 100644 index 0000000..4db1be1 --- /dev/null +++ b/install_tv_windows.bat @@ -0,0 +1,5 @@ +@echo off +cd /d %~dp0 +adb connect 192.168.1.56:5555 +adb install -r tv_keyboard\build\outputs\apk\debug\tv_keyboard-debug.apk +pause diff --git a/phone_remote/build.gradle b/phone_remote/build.gradle new file mode 100644 index 0000000..243fdbf --- /dev/null +++ b/phone_remote/build.gradle @@ -0,0 +1,14 @@ +plugins { id 'com.android.application' } + +android { + namespace 'com.ando.miboxkeyboard.phone' + compileSdk 35 + + defaultConfig { + applicationId 'com.ando.miboxkeyboard.phone' + minSdk 23 + targetSdk 35 + versionCode 1 + versionName '1.0' + } +} diff --git a/phone_remote/src/main/AndroidManifest.xml b/phone_remote/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8addb31 --- /dev/null +++ b/phone_remote/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/phone_remote/src/main/java/com/ando/miboxkeyboard/phone/MainActivity.java b/phone_remote/src/main/java/com/ando/miboxkeyboard/phone/MainActivity.java new file mode 100644 index 0000000..e138d30 --- /dev/null +++ b/phone_remote/src/main/java/com/ando/miboxkeyboard/phone/MainActivity.java @@ -0,0 +1,302 @@ +package com.ando.miboxkeyboard.phone; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class MainActivity extends Activity { + private static final String TAG = "AndoPhone"; + private EditText ipEdit; + private EditText portEdit; + private EditText textEdit; + private TextView statusText; + private SharedPreferences prefs; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + prefs = getSharedPreferences("settings", MODE_PRIVATE); + + LinearLayout root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); + root.setPadding(32, 32, 32, 32); + + TextView title = new TextView(this); + title.setText("Clavier MiBox Téléphone"); + title.setTextSize(24); + title.setGravity(Gravity.CENTER); + root.addView(title, new LinearLayout.LayoutParams(-1, -2)); + + ipEdit = new EditText(this); + ipEdit.setHint("IP Mi Box"); + ipEdit.setSingleLine(true); + ipEdit.setText(prefs.getString("ip", "192.168.1.8")); + root.addView(ipEdit, new LinearLayout.LayoutParams(-1, -2)); + + portEdit = new EditText(this); + portEdit.setHint("Port"); + portEdit.setSingleLine(true); + portEdit.setText(prefs.getString("port", "8765")); + root.addView(portEdit, new LinearLayout.LayoutParams(-1, -2)); + + textEdit = new EditText(this); + textEdit.setHint("Texte à envoyer vers la Mi Box"); + textEdit.setMinLines(5); + textEdit.setGravity(Gravity.TOP | Gravity.START); + root.addView(textEdit, new LinearLayout.LayoutParams(-1, 0, 1)); + + Button sendButton = new Button(this); + sendButton.setText("Envoyer le texte"); + sendButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveSettings(); + sendText(textEdit.getText().toString()); + } + }); + root.addView(sendButton, new LinearLayout.LayoutParams(-1, -2)); + + LinearLayout row = new LinearLayout(this); + row.setOrientation(LinearLayout.HORIZONTAL); + + Button enterButton = new Button(this); + enterButton.setText("Entrée"); + enterButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveSettings(); + callPostEndpoint("/enter", ""); + } + }); + row.addView(enterButton, new LinearLayout.LayoutParams(0, -2, 1)); + + Button backspaceButton = new Button(this); + backspaceButton.setText("Effacer"); + backspaceButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveSettings(); + callPostEndpoint("/backspace", ""); + } + }); + row.addView(backspaceButton, new LinearLayout.LayoutParams(0, -2, 1)); + + Button pingButton = new Button(this); + pingButton.setText("Tester"); + pingButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveSettings(); + callGetEndpoint("/ping"); + } + }); + row.addView(pingButton, new LinearLayout.LayoutParams(0, -2, 1)); + + root.addView(row, new LinearLayout.LayoutParams(-1, -2)); + + LinearLayout keyboardRow = new LinearLayout(this); + keyboardRow.setOrientation(LinearLayout.HORIZONTAL); + + Button pickerButton = new Button(this); + pickerButton.setText("Changer clavier Mi Box"); + pickerButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveSettings(); + callPostEndpoint("/picker", ""); + } + }); + keyboardRow.addView(pickerButton, new LinearLayout.LayoutParams(0, -2, 1)); + + Button settingsButton = new Button(this); + settingsButton.setText("Paramètres clavier"); + settingsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + saveSettings(); + callGetEndpoint("/settings"); + } + }); + keyboardRow.addView(settingsButton, new LinearLayout.LayoutParams(0, -2, 1)); + + root.addView(keyboardRow, new LinearLayout.LayoutParams(-1, -2)); + + Button clearButton = new Button(this); + clearButton.setText("Vider le champ téléphone"); + clearButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + textEdit.setText(""); + } + }); + root.addView(clearButton, new LinearLayout.LayoutParams(-1, -2)); + + statusText = new TextView(this); + statusText.setText("Statut : prêt"); + statusText.setPadding(0, 20, 0, 0); + root.addView(statusText, new LinearLayout.LayoutParams(-1, -2)); + + setContentView(root); + } + + private void saveSettings() { + prefs.edit() + .putString("ip", ipEdit.getText().toString().trim()) + .putString("port", portEdit.getText().toString().trim()) + .apply(); + } + + private void sendText(String text) { + callPostEndpoint("/type", text); + } + + private String baseUrl(String endpoint) throws Exception { + String ip = ipEdit.getText().toString().trim(); + String port = portEdit.getText().toString().trim(); + if (ip.startsWith("http://")) ip = ip.substring(7); + if (ip.startsWith("https://")) ip = ip.substring(8); + if (ip.endsWith("/")) ip = ip.substring(0, ip.length() - 1); + if (ip.contains(":")) { + return "http://" + ip + endpoint; + } + return "http://" + ip + ":" + port + endpoint; + } + + + private String previewUrl(String endpoint) { + try { + return baseUrl(endpoint); + } catch (Exception e) { + return endpoint; + } + } + + private void callGetEndpoint(final String endpoint) { + setStatus("GET " + endpoint + " → " + previewUrl(endpoint)); + Log.d(TAG, "GET " + previewUrl(endpoint)); + new Thread(new Runnable() { + @Override + public void run() { + HttpURLConnection connection = null; + try { + URL url = new URL(baseUrl(endpoint)); + Log.d(TAG, "Opening POST " + url); + Log.d(TAG, "Opening GET " + url); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(4000); + connection.setReadTimeout(4000); + connection.setDoInput(true); + + int code = connection.getResponseCode(); + String response = readResponse(connection); + if (code >= 200 && code < 300) { + Log.d(TAG, endpoint + " OK " + code + " " + response); + setStatus("OK : " + response); + } else { + Log.e(TAG, endpoint + " HTTP " + code + " " + response); + setStatus("Erreur HTTP " + code + " : " + response); + } + } catch (Exception e) { + Log.e(TAG, endpoint + " exception", e); + setStatus("Erreur : " + e.getClass().getSimpleName() + " - " + e.getMessage()); + showToast("Connexion impossible. Vérifie IP, Wi-Fi et serveur Mi Box."); + } finally { + if (connection != null) connection.disconnect(); + } + } + }).start(); + } + + private void callPostEndpoint(final String endpoint, final String body) { + setStatus("POST " + endpoint + " → " + previewUrl(endpoint)); + Log.d(TAG, "POST " + previewUrl(endpoint)); + new Thread(new Runnable() { + @Override + public void run() { + HttpURLConnection connection = null; + try { + URL url = new URL(baseUrl(endpoint)); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setConnectTimeout(4000); + connection.setReadTimeout(4000); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "text/plain; charset=utf-8"); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(bytes.length); + OutputStream out = connection.getOutputStream(); + out.write(bytes); + out.flush(); + out.close(); + + int code = connection.getResponseCode(); + String response = readResponse(connection); + if (code >= 200 && code < 300) { + Log.d(TAG, endpoint + " OK " + code + " " + response); + setStatus("OK : " + response); + } else { + Log.e(TAG, endpoint + " HTTP " + code + " " + response); + setStatus("Erreur HTTP " + code + " : " + response); + } + } catch (Exception e) { + Log.e(TAG, endpoint + " exception", e); + setStatus("Erreur : " + e.getClass().getSimpleName() + " - " + e.getMessage()); + showToast("Connexion impossible. Vérifie IP, Wi-Fi et clavier actif."); + } finally { + if (connection != null) connection.disconnect(); + } + } + }).start(); + } + + private String readResponse(HttpURLConnection connection) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + if (sb.length() > 0) sb.append(" "); + sb.append(line); + } + reader.close(); + return sb.toString(); + } catch (Exception e) { + return ""; + } + } + + private void setStatus(final String status) { + runOnUiThread(new Runnable() { + @Override + public void run() { + statusText.setText("Statut : " + status); + } + }); + } + + private void showToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); + } + }); + } +} diff --git a/phone_remote/src/main/res/values/strings.xml b/phone_remote/src/main/res/values/strings.xml new file mode 100644 index 0000000..67c0e0a --- /dev/null +++ b/phone_remote/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Clavier MiBox Téléphone + diff --git a/phone_remote/src/main/res/values/styles.xml b/phone_remote/src/main/res/values/styles.xml new file mode 100644 index 0000000..0f55d27 --- /dev/null +++ b/phone_remote/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + diff --git a/phone_remote/src/main/res/xml/network_security_config.xml b/phone_remote/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..287db2a --- /dev/null +++ b/phone_remote/src/main/res/xml/network_security_config.xml @@ -0,0 +1,3 @@ + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0d429d2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS); repositories { google(); mavenCentral() } } +rootProject.name = 'ClavierAndoMiBox' +include ':tv_keyboard' +include ':phone_remote' diff --git a/tv_keyboard/build.gradle b/tv_keyboard/build.gradle new file mode 100644 index 0000000..ff046cc --- /dev/null +++ b/tv_keyboard/build.gradle @@ -0,0 +1,14 @@ +plugins { id 'com.android.application' } + +android { + namespace 'com.ando.miboxkeyboard.tv' + compileSdk 35 + + defaultConfig { + applicationId 'com.ando.miboxkeyboard.tv' + minSdk 23 + targetSdk 35 + versionCode 1 + versionName '1.0' + } +} diff --git a/tv_keyboard/src/main/AndroidManifest.xml b/tv_keyboard/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2634467 --- /dev/null +++ b/tv_keyboard/src/main/AndroidManifest.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/AndoServerService.java b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/AndoServerService.java new file mode 100644 index 0000000..008f71c --- /dev/null +++ b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/AndoServerService.java @@ -0,0 +1,74 @@ +package com.ando.miboxkeyboard.tv; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; + +public class AndoServerService extends Service { + private static final String CHANNEL_ID = "ando_mibox_keyboard_server"; + private static final int NOTIFICATION_ID = 8765; + + @Override + public void onCreate() { + super.onCreate(); + startAsForeground(); + RemoteCommandServer.start(this); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startAsForeground(); + RemoteCommandServer.start(this); + return START_STICKY; + } + + private void startAsForeground() { + try { + createNotificationChannel(); + Notification notification; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notification = new Notification.Builder(this, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("Clavier Ando MiBox") + .setContentText("Serveur clavier actif sur le port 8765") + .setOngoing(true) + .build(); + } else { + notification = new Notification.Builder(this) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("Clavier Ando MiBox") + .setContentText("Serveur clavier actif sur le port 8765") + .setOngoing(true) + .build(); + } + startForeground(NOTIFICATION_ID, notification); + } catch (Exception ignored) { + // If foreground mode is refused by the system, keep trying to start the HTTP server anyway. + } + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (manager != null) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "Serveur Clavier Ando MiBox", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("Maintient le serveur clavier actif pour le smartphone."); + manager.createNotificationChannel(channel); + } + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/BootReceiver.java b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/BootReceiver.java new file mode 100644 index 0000000..c380637 --- /dev/null +++ b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/BootReceiver.java @@ -0,0 +1,34 @@ +package com.ando.miboxkeyboard.tv; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) return; + String action = intent.getAction(); + if (Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) { + startServer(context); + } + } + + static void startServer(Context context) { + if (context == null) return; + Intent serviceIntent = new Intent(context, AndoServerService.class); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent); + } else { + context.startService(serviceIntent); + } + } catch (Exception ignored) { + try { + context.startService(serviceIntent); + } catch (Exception ignoredAgain) { + } + } + } +} diff --git a/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/PickerActivity.java b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/PickerActivity.java new file mode 100644 index 0000000..b8b9138 --- /dev/null +++ b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/PickerActivity.java @@ -0,0 +1,35 @@ +package com.ando.miboxkeyboard.tv; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.inputmethod.InputMethodManager; + +public class PickerActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + try { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showInputMethodPicker(); + } + } catch (Exception ignored) { + } + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 1200); + } + }, 300); + } +} diff --git a/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/RemoteCommandServer.java b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/RemoteCommandServer.java new file mode 100644 index 0000000..f73787a --- /dev/null +++ b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/RemoteCommandServer.java @@ -0,0 +1,209 @@ +package com.ando.miboxkeyboard.tv; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class RemoteCommandServer { + private static final int PORT = 8765; + private static final Object LOCK = new Object(); + private static volatile boolean serverStarted = false; + private static Context appContext; + private static Thread serverThread; + + private RemoteCommandServer() {} + + public static void start(Context context) { + if (context != null) { + appContext = context.getApplicationContext(); + } + synchronized (LOCK) { + if (serverStarted) return; + serverStarted = true; + serverThread = new Thread(new Runnable() { + @Override + public void run() { + runServer(); + } + }, "AndoMiBoxAlwaysOnHttpServer"); + serverThread.setDaemon(true); + serverThread.start(); + } + } + + private static void runServer() { + try (ServerSocket serverSocket = new ServerSocket(PORT)) { + while (true) { + Socket socket = serverSocket.accept(); + handleClient(socket); + } + } catch (Exception ignored) { + serverStarted = false; + } + } + + private static void handleClient(Socket socket) { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + String requestLine = reader.readLine(); + if (requestLine == null) { + socket.close(); + return; + } + + int contentLength = 0; + String line; + while ((line = reader.readLine()) != null && line.length() > 0) { + String lower = line.toLowerCase(); + if (lower.startsWith("content-length:")) { + contentLength = Integer.parseInt(line.substring(15).trim()); + } + } + + char[] bodyChars = new char[Math.max(contentLength, 0)]; + int read = 0; + while (read < contentLength) { + int r = reader.read(bodyChars, read, contentLength - read); + if (r < 0) break; + read += r; + } + String body = new String(bodyChars, 0, read); + + String response; + int status = 200; + Runnable afterResponse = null; + + if (requestLine.startsWith("OPTIONS ")) { + response = "OK options"; + } else if (requestLine.contains("/type")) { + String text = extractText(requestLine, body); + boolean ok = RemoteKeyboardImeService.typeFromServer(text); + response = ok ? "OK typed" : "NO active keyboard"; + } else if (requestLine.contains("/enter")) { + boolean ok = RemoteKeyboardImeService.sendKeyFromServer(KeyEvent.KEYCODE_ENTER); + response = ok ? "OK enter" : "NO active keyboard"; + } else if (requestLine.contains("/backspace")) { + boolean ok = RemoteKeyboardImeService.sendKeyFromServer(KeyEvent.KEYCODE_DEL); + response = ok ? "OK backspace" : "NO active keyboard"; + } else if (requestLine.contains("/picker")) { + response = "OK picker"; + afterResponse = new Runnable() { + @Override + public void run() { + showKeyboardPickerDelayed(); + } + }; + } else if (requestLine.contains("/settings")) { + response = "OK settings"; + afterResponse = new Runnable() { + @Override + public void run() { + openKeyboardSettingsDelayed(); + } + }; + } else if (requestLine.contains("/ping")) { + response = "OK Clavier Ando MiBox server"; + } else { + status = 404; + response = "Not found"; + } + + writeResponse(socket, status, response); + + if (afterResponse != null) { + afterResponse.run(); + } + } catch (Exception ignored) { + try { writeResponse(socket, 500, "Error"); } catch (Exception ignoredAgain) {} + } finally { + try { socket.close(); } catch (Exception ignored) {} + } + } + + private static String extractText(String requestLine, String body) throws Exception { + String text = body == null ? "" : body; + + int idx = requestLine.indexOf("text="); + if (idx >= 0) { + String part = requestLine.substring(idx + 5); + int end = part.indexOf(' '); + if (end >= 0) part = part.substring(0, end); + int amp = part.indexOf('&'); + if (amp >= 0) part = part.substring(0, amp); + text = URLDecoder.decode(part, "UTF-8"); + } + + if (text.trim().startsWith("{")) { + Matcher m = Pattern.compile("\\\"text\\\"\\s*:\\s*\\\"(.*?)\\\"").matcher(text); + if (m.find()) { + text = m.group(1) + .replace("\\n", "\n") + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + } + return text; + } + + private static void showKeyboardPickerDelayed() { + if (appContext == null) return; + + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + try { + Intent intent = new Intent(appContext, PickerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appContext.startActivity(intent); + } catch (Exception ignored) { + } + } + }, 300); + } + + private static void openKeyboardSettingsDelayed() { + final Context context = appContext; + if (context == null) return; + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + try { + Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception ignored) {} + } + }, 250); + } + + private static void writeResponse(Socket socket, int status, String body) throws Exception { + String statusText = status == 200 ? "OK" : status == 404 ? "Not Found" : "Internal Server Error"; + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + String headers = "HTTP/1.1 " + status + " " + statusText + "\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Access-Control-Allow-Origin: *\r\n" + + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + + "Access-Control-Allow-Headers: Content-Type\r\n" + + "Content-Length: " + bytes.length + "\r\n" + + "Connection: close\r\n\r\n"; + OutputStream out = socket.getOutputStream(); + out.write(headers.getBytes(StandardCharsets.UTF_8)); + out.write(bytes); + out.flush(); + } +} + diff --git a/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/RemoteKeyboardImeService.java b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/RemoteKeyboardImeService.java new file mode 100644 index 0000000..dcdeba1 --- /dev/null +++ b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/RemoteKeyboardImeService.java @@ -0,0 +1,69 @@ +package com.ando.miboxkeyboard.tv; + +import android.inputmethodservice.InputMethodService; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.InputConnection; +import android.widget.TextView; + +public class RemoteKeyboardImeService extends InputMethodService { + private static RemoteKeyboardImeService activeService; + + @Override + public void onCreate() { + super.onCreate(); + activeService = this; + RemoteCommandServer.start(this); + BootReceiver.startServer(this); + } + + @Override + public void onDestroy() { + if (activeService == this) activeService = null; + BootReceiver.startServer(this); + super.onDestroy(); + } + + @Override + public View onCreateInputView() { + BootReceiver.startServer(this); + TextView view = new TextView(this); + view.setText("Clavier Ando MiBox actif - envoyer depuis le téléphone"); + view.setTextSize(16); + view.setPadding(20, 20, 20, 20); + return view; + } + + public static boolean typeFromServer(final String text) { + final RemoteKeyboardImeService service = activeService; + if (service == null || text == null) return false; + service.runOnMainThread(new Runnable() { + @Override + public void run() { + InputConnection ic = service.getCurrentInputConnection(); + if (ic != null) ic.commitText(text, 1); + } + }); + return true; + } + + public static boolean sendKeyFromServer(final int keyCode) { + final RemoteKeyboardImeService service = activeService; + if (service == null) return false; + service.runOnMainThread(new Runnable() { + @Override + public void run() { + InputConnection ic = service.getCurrentInputConnection(); + if (ic != null) { + ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode)); + } + } + }); + return true; + } + + private void runOnMainThread(Runnable runnable) { + new android.os.Handler(getMainLooper()).post(runnable); + } +} diff --git a/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/SetupActivity.java b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/SetupActivity.java new file mode 100644 index 0000000..8f20331 --- /dev/null +++ b/tv_keyboard/src/main/java/com/ando/miboxkeyboard/tv/SetupActivity.java @@ -0,0 +1,86 @@ +package com.ando.miboxkeyboard.tv; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.provider.Settings; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class SetupActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + RemoteCommandServer.start(this); + BootReceiver.startServer(this); + + LinearLayout root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); + root.setPadding(40, 40, 40, 40); + root.setGravity(Gravity.CENTER_HORIZONTAL); + + TextView title = new TextView(this); + title.setText("Clavier Ando MiBox"); + title.setTextSize(28); + title.setGravity(Gravity.CENTER); + root.addView(title, new LinearLayout.LayoutParams(-1, -2)); + + TextView info = new TextView(this); + info.setText("Serveur en premier plan actif sur le port 8765.\n\n" + + "Le bouton du smartphone peut ouvrir le sélecteur clavier même si Gboard est actif et même depuis une autre app.\n\n" + + "Commandes :\n" + + "- /picker : choisir le clavier actif\n" + + "- /settings : paramètres clavier\n" + + "- /type : écrire si Clavier Ando MiBox est actif\n" + + "- /enter et /backspace"); + info.setTextSize(18); + info.setPadding(0, 30, 0, 30); + root.addView(info, new LinearLayout.LayoutParams(-1, -2)); + + Button startServer = new Button(this); + startServer.setText("Démarrer / garder le serveur actif"); + startServer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + RemoteCommandServer.start(SetupActivity.this); + BootReceiver.startServer(SetupActivity.this); + } + }); + root.addView(startServer, new LinearLayout.LayoutParams(-1, -2)); + + Button openSettings = new Button(this); + openSettings.setText("Ouvrir les paramètres clavier"); + openSettings.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)); + } + }); + root.addView(openSettings, new LinearLayout.LayoutParams(-1, -2)); + + Button chooseKeyboard = new Button(this); + chooseKeyboard.setText("Choisir le clavier actif"); + chooseKeyboard.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + RemoteCommandServer.start(SetupActivity.this); + BootReceiver.startServer(SetupActivity.this); + ((android.view.inputmethod.InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).showInputMethodPicker(); + } + }); + root.addView(chooseKeyboard, new LinearLayout.LayoutParams(-1, -2)); + + setContentView(root); + } + + @Override + protected void onResume() { + super.onResume(); + RemoteCommandServer.start(this); + BootReceiver.startServer(this); + } +} diff --git a/tv_keyboard/src/main/res/values/strings.xml b/tv_keyboard/src/main/res/values/strings.xml new file mode 100644 index 0000000..b64b671 --- /dev/null +++ b/tv_keyboard/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Clavier Ando MiBox + diff --git a/tv_keyboard/src/main/res/values/styles.xml b/tv_keyboard/src/main/res/values/styles.xml new file mode 100644 index 0000000..536aca0 --- /dev/null +++ b/tv_keyboard/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + diff --git a/tv_keyboard/src/main/res/xml/method.xml b/tv_keyboard/src/main/res/xml/method.xml new file mode 100644 index 0000000..7de42d3 --- /dev/null +++ b/tv_keyboard/src/main/res/xml/method.xml @@ -0,0 +1,10 @@ + + + + +