Initial upload: clavier-ando-mibox

This commit is contained in:
Abderrahim-Mouhcine
2026-08-16 19:57:54 +01:00
commit a677cc3f26
35 changed files with 1660 additions and 0 deletions
+14
View File
@@ -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'
}
}
+62
View File
@@ -0,0 +1,62 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="false"
android:label="Clavier Ando MiBox"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name=".SetupActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".PickerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:exported="true" />
<service
android:name=".AndoServerService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".RemoteKeyboardImeService"
android:directBootAware="true"
android:exported="true"
android:label="Clavier Ando MiBox"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data
android:name="android.view.im"
android:resource="@xml/method" />
</service>
</application>
</manifest>
@@ -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;
}
}
@@ -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) {
}
}
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Clavier Ando MiBox</string>
</resources>
@@ -0,0 +1,6 @@
<resources>
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>
+10
View File
@@ -0,0 +1,10 @@
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="com.ando.miboxkeyboard.tv.SetupActivity"
android:supportsSwitchingToNextInputMethod="true">
<subtype
android:label="@string/app_name"
android:imeSubtypeLocale="fr_FR"
android:imeSubtypeMode="keyboard" />
</input-method>