feat(android): add android support
@@ -3,6 +3,13 @@ plugins {
|
||||
id 'com.chaquo.python'
|
||||
}
|
||||
|
||||
def repoRoot = rootProject.projectDir.parentFile
|
||||
def meshchatSourceDir = new File(repoRoot, "meshchatx")
|
||||
def chaquopyBuildPython = System.getenv("CHAQUOPY_BUILD_PYTHON")
|
||||
def pythonTempDir = new File(buildDir, "python-tmp")
|
||||
def vendorWheelDir = new File(rootProject.projectDir, "vendor")
|
||||
def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.4.6-py3-none-any.whl")
|
||||
|
||||
android {
|
||||
namespace 'com.meshchatx'
|
||||
compileSdk 34
|
||||
@@ -22,7 +29,8 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
@@ -39,21 +47,80 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("syncMeshchatPython", Sync) {
|
||||
if (!meshchatSourceDir.exists()) {
|
||||
throw new org.gradle.api.GradleException("Missing meshchatx source directory at ${meshchatSourceDir}")
|
||||
}
|
||||
doFirst {
|
||||
// Remove legacy vendored packages so pip-installed dependencies are used.
|
||||
delete(
|
||||
file("$projectDir/src/main/python/LXST"),
|
||||
file("$projectDir/src/main/python/LXMF"),
|
||||
file("$projectDir/src/main/python/RNS")
|
||||
)
|
||||
}
|
||||
from(meshchatSourceDir)
|
||||
includeEmptyDirs = false
|
||||
into(file("$projectDir/src/main/python/meshchatx"))
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(tasks.named("syncMeshchatPython"))
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.toLowerCase().contains("merge") && task.name.toLowerCase().contains("pythonsources")) {
|
||||
task.dependsOn(tasks.named("syncMeshchatPython"))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.toLowerCase().contains("python")) {
|
||||
task.doFirst {
|
||||
if (!pythonTempDir.exists()) {
|
||||
pythonTempDir.mkdirs()
|
||||
}
|
||||
if (task.metaClass.respondsTo(task, "environment", String, Object)) {
|
||||
task.environment("TMPDIR", pythonTempDir.absolutePath)
|
||||
task.environment("TEMP", pythonTempDir.absolutePath)
|
||||
task.environment("TMP", pythonTempDir.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chaquopy {
|
||||
defaultConfig {
|
||||
version = "3.11"
|
||||
buildPython "/usr/bin/python3.11"
|
||||
|
||||
version "3.11"
|
||||
|
||||
if (chaquopyBuildPython != null && !chaquopyBuildPython.trim().isEmpty()) {
|
||||
buildPython chaquopyBuildPython
|
||||
}
|
||||
|
||||
pip {
|
||||
if (!vendorWheelDir.exists()) {
|
||||
throw new org.gradle.api.GradleException("Missing vendor wheel directory at ${vendorWheelDir}")
|
||||
}
|
||||
options "--find-links", vendorWheelDir.absolutePath
|
||||
install "aiohttp==3.10.10"
|
||||
install "rns>=1.1.5"
|
||||
install "lxmf>=0.9.4"
|
||||
install "numpy==1.26.2"
|
||||
install "chaquopy-libcodec2==1.2.0"
|
||||
install "pycodec2==4.1.1"
|
||||
if (!lxstPatchedWheel.exists()) {
|
||||
throw new org.gradle.api.GradleException("Missing patched LXST wheel at ${lxstPatchedWheel}")
|
||||
}
|
||||
install lxstPatchedWheel.absolutePath
|
||||
install "psutil>=7.1.3"
|
||||
install "websockets>=15.0.1"
|
||||
install "bcrypt==3.1.7"
|
||||
install "aiohttp-session>=2.12.1,<3.0.0"
|
||||
install "cryptography==42.0.8"
|
||||
install "numpy==1.26.2"
|
||||
install "pycparser>=3.0"
|
||||
install "pyserial>=3.5"
|
||||
install "jaraco.context>=6.1.1"
|
||||
install "ply>=3.11,<4.0"
|
||||
install "pycodec2-3.0.1-cp311-cp311-linux_aarch64.whl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-keep class com.chaquo.python.** { *; }
|
||||
-keep class com.meshchatx.** { *; }
|
||||
-keep class org.json.** { *; }
|
||||
-keep class org.conscrypt.** { *; }
|
||||
-dontwarn com.chaquo.python.**
|
||||
-dontwarn org.conscrypt.**
|
||||
@@ -8,13 +8,19 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||
|
||||
<application
|
||||
android:name="com.chaquo.python.android.PyApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/ic_launcher_round"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MeshChatX"
|
||||
android:usesCleartextTraffic="true"
|
||||
@@ -31,11 +37,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".MeshChatService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,20 +1,76 @@
|
||||
package com.meshchatx;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.webkit.PermissionRequest;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceError;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.chaquo.python.Python;
|
||||
import com.chaquo.python.android.AndroidPlatform;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private WebView webView;
|
||||
private ProgressBar progressBar;
|
||||
private TextView loadingText;
|
||||
private TextView errorText;
|
||||
private static final String SERVER_URL = "https://127.0.0.1:8000";
|
||||
private static final int SERVER_PORT = 8000;
|
||||
private static final int RUNTIME_PERMISSIONS_REQUEST_CODE = 1001;
|
||||
private static final int MAX_CONNECTION_ATTEMPTS = 30;
|
||||
private static final long CONNECTION_RETRY_DELAY_MS = 1000;
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
private PermissionRequest pendingWebPermissionRequest = null;
|
||||
private ValueCallback<Uri[]> filePathCallback = null;
|
||||
private boolean startupPageLoaded = false;
|
||||
private boolean backendFailed = false;
|
||||
private int connectionAttempts = 0;
|
||||
private final ActivityResultLauncher<Intent> filePickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
Uri[] selection = null;
|
||||
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
|
||||
Intent data = result.getData();
|
||||
if (data.getClipData() != null) {
|
||||
int count = data.getClipData().getItemCount();
|
||||
selection = new Uri[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
selection[i] = data.getClipData().getItemAt(i).getUri();
|
||||
}
|
||||
} else if (data.getData() != null) {
|
||||
selection = new Uri[] { data.getData() };
|
||||
}
|
||||
}
|
||||
if (filePathCallback != null) {
|
||||
filePathCallback.onReceiveValue(selection);
|
||||
filePathCallback = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
@@ -24,10 +80,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
webView = findViewById(R.id.webView);
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
loadingText = findViewById(R.id.loadingText);
|
||||
errorText = findViewById(R.id.errorText);
|
||||
showLoading("Starting MeshChatX backend...");
|
||||
|
||||
if (!Python.isStarted()) {
|
||||
Python.start(new AndroidPlatform(this));
|
||||
}
|
||||
requestRuntimePermissionsIfNeeded();
|
||||
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
@@ -41,7 +101,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
super.onPageFinished(view, url);
|
||||
startupPageLoaded = true;
|
||||
mainHandler.removeCallbacksAndMessages(null);
|
||||
progressBar.setVisibility(android.view.View.GONE);
|
||||
loadingText.setVisibility(android.view.View.GONE);
|
||||
errorText.setVisibility(android.view.View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +114,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
progressBar.setVisibility(android.view.View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
||||
super.onReceivedError(view, request, error);
|
||||
if (request != null && request.isForMainFrame() && isStartupRequest(request.getUrl().toString())) {
|
||||
if (backendFailed && !startupPageLoaded) {
|
||||
CharSequence description = (error != null) ? error.getDescription() : "Unknown error";
|
||||
showStartupError("WebView failed to load MeshChatX: " + description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl);
|
||||
if (isStartupRequest(failingUrl) && !startupPageLoaded) {
|
||||
if (backendFailed) {
|
||||
showStartupError("WebView failed to load MeshChatX: " + description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WebViewClientOnReceivedSslError")
|
||||
@Override
|
||||
public void onReceivedSslError(WebView view, android.webkit.SslErrorHandler handler, android.net.http.SslError error) {
|
||||
@@ -57,32 +142,200 @@ public class MainActivity extends AppCompatActivity {
|
||||
handler.proceed();
|
||||
}
|
||||
});
|
||||
webView.setWebChromeClient(new WebChromeClient() {
|
||||
@Override
|
||||
public void onPermissionRequest(final PermissionRequest request) {
|
||||
runOnUiThread(() -> {
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean needsAudioCapture = false;
|
||||
for (String resource : request.getResources()) {
|
||||
if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(resource)) {
|
||||
needsAudioCapture = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsAudioCapture) {
|
||||
request.grant(request.getResources());
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
request.grant(request.getResources());
|
||||
return;
|
||||
}
|
||||
|
||||
pendingWebPermissionRequest = request;
|
||||
requestRuntimePermissionsIfNeeded();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShowFileChooser(
|
||||
WebView webView,
|
||||
ValueCallback<Uri[]> filePathCallback,
|
||||
WebChromeClient.FileChooserParams fileChooserParams
|
||||
) {
|
||||
if (MainActivity.this.filePathCallback != null) {
|
||||
MainActivity.this.filePathCallback.onReceiveValue(null);
|
||||
}
|
||||
MainActivity.this.filePathCallback = filePathCallback;
|
||||
|
||||
Intent chooserIntent;
|
||||
try {
|
||||
chooserIntent = fileChooserParams != null
|
||||
? fileChooserParams.createIntent()
|
||||
: new Intent(Intent.ACTION_GET_CONTENT);
|
||||
} catch (Exception e) {
|
||||
chooserIntent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
}
|
||||
chooserIntent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
if (chooserIntent.getType() == null) {
|
||||
chooserIntent.setType("*/*");
|
||||
}
|
||||
if (fileChooserParams != null && fileChooserParams.getAcceptTypes() != null) {
|
||||
chooserIntent.putExtra(Intent.EXTRA_MIME_TYPES, fileChooserParams.getAcceptTypes());
|
||||
}
|
||||
chooserIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
|
||||
try {
|
||||
filePickerLauncher.launch(chooserIntent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
if (MainActivity.this.filePathCallback != null) {
|
||||
MainActivity.this.filePathCallback.onReceiveValue(null);
|
||||
MainActivity.this.filePathCallback = null;
|
||||
}
|
||||
Toast.makeText(MainActivity.this, "No file picker available", Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
startMeshChatServer();
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
scheduleConnectionRetry("Connecting to local server...");
|
||||
}
|
||||
|
||||
private void requestRuntimePermissionsIfNeeded() {
|
||||
List<String> missingPermissions = new ArrayList<>();
|
||||
addIfMissing(missingPermissions, Manifest.permission.RECORD_AUDIO);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
addIfMissing(missingPermissions, Manifest.permission.BLUETOOTH_CONNECT);
|
||||
addIfMissing(missingPermissions, Manifest.permission.BLUETOOTH_SCAN);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
addIfMissing(missingPermissions, Manifest.permission.POST_NOTIFICATIONS);
|
||||
}
|
||||
if (!missingPermissions.isEmpty()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
missingPermissions.toArray(new String[0]),
|
||||
RUNTIME_PERMISSIONS_REQUEST_CODE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void addIfMissing(List<String> missingPermissions, String permission) {
|
||||
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
missingPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode != RUNTIME_PERMISSIONS_REQUEST_CODE) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
|
||||
if (Manifest.permission.RECORD_AUDIO.equals(permissions[i]) && pendingWebPermissionRequest != null) {
|
||||
pendingWebPermissionRequest.deny();
|
||||
pendingWebPermissionRequest = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
webView.loadUrl(SERVER_URL);
|
||||
});
|
||||
}).start();
|
||||
}
|
||||
if (pendingWebPermissionRequest != null) {
|
||||
pendingWebPermissionRequest.grant(pendingWebPermissionRequest.getResources());
|
||||
pendingWebPermissionRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startMeshChatServer() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Python py = Python.getInstance();
|
||||
py.getModule("meshchat_wrapper").callAttr("start_server", SERVER_PORT);
|
||||
String appFilesDir = getFilesDir().getAbsolutePath();
|
||||
py.getModule("meshchat_wrapper").callAttr("start_server", SERVER_PORT, appFilesDir);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
backendFailed = true;
|
||||
showStartupError("MeshChatX backend failed:\n" + toStackTrace(e));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private boolean isStartupRequest(String url) {
|
||||
return url != null && url.startsWith(SERVER_URL);
|
||||
}
|
||||
|
||||
private void scheduleConnectionRetry(String message) {
|
||||
if (startupPageLoaded || backendFailed) {
|
||||
return;
|
||||
}
|
||||
showLoading(message + " (" + (connectionAttempts + 1) + "/" + MAX_CONNECTION_ATTEMPTS + ")");
|
||||
mainHandler.postDelayed(() -> {
|
||||
if (startupPageLoaded || backendFailed) {
|
||||
return;
|
||||
}
|
||||
connectionAttempts += 1;
|
||||
if (connectionAttempts > MAX_CONNECTION_ATTEMPTS) {
|
||||
showStartupError("Failed to connect to local MeshChatX server.");
|
||||
return;
|
||||
}
|
||||
webView.loadUrl(SERVER_URL);
|
||||
scheduleConnectionRetry("Retrying connection...");
|
||||
}, CONNECTION_RETRY_DELAY_MS);
|
||||
}
|
||||
|
||||
private String toStackTrace(Throwable error) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
error.printStackTrace(pw);
|
||||
pw.flush();
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
private void showStartupError(String message) {
|
||||
runOnUiThread(() -> {
|
||||
mainHandler.removeCallbacksAndMessages(null);
|
||||
progressBar.setVisibility(android.view.View.GONE);
|
||||
loadingText.setVisibility(android.view.View.GONE);
|
||||
if (errorText != null) {
|
||||
errorText.setText(message);
|
||||
errorText.setVisibility(android.view.View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showLoading(String message) {
|
||||
runOnUiThread(() -> {
|
||||
if (startupPageLoaded) {
|
||||
return;
|
||||
}
|
||||
progressBar.setVisibility(android.view.View.VISIBLE);
|
||||
errorText.setVisibility(android.view.View.GONE);
|
||||
if (loadingText != null) {
|
||||
loadingText.setText(message);
|
||||
loadingText.setVisibility(android.view.View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
@@ -95,6 +348,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
mainHandler.removeCallbacksAndMessages(null);
|
||||
if (pendingWebPermissionRequest != null) {
|
||||
pendingWebPermissionRequest.deny();
|
||||
pendingWebPermissionRequest = null;
|
||||
}
|
||||
if (filePathCallback != null) {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
filePathCallback = null;
|
||||
}
|
||||
if (webView != null) {
|
||||
webView.destroy();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,94 @@
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
|
||||
def start_server(port=8000):
|
||||
def _ensure_android_reticulum_config(reticulum_config_dir):
|
||||
if not reticulum_config_dir:
|
||||
return
|
||||
|
||||
config_path = os.path.join(reticulum_config_dir, "config")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, encoding="utf-8") as existing_file:
|
||||
content = existing_file.read()
|
||||
if "share_instance = Yes" in content:
|
||||
content = content.replace("share_instance = Yes", "share_instance = No")
|
||||
with open(config_path, "w", encoding="utf-8") as config_file:
|
||||
config_file.write(content)
|
||||
return
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as config_file:
|
||||
config_file.write("[reticulum]\n share_instance = No\n\n[interfaces]\n")
|
||||
|
||||
|
||||
def _patch_asyncio_signal_handlers_for_android():
|
||||
try:
|
||||
from asyncio import unix_events
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
loop_cls = getattr(unix_events, "_UnixSelectorEventLoop", None)
|
||||
if loop_cls is None:
|
||||
return None
|
||||
|
||||
original_add_signal_handler = loop_cls.add_signal_handler
|
||||
|
||||
def _safe_add_signal_handler(self, sig, callback, *args):
|
||||
try:
|
||||
return original_add_signal_handler(self, sig, callback, *args)
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
message = str(exc)
|
||||
if "set_wakeup_fd only works in main thread" in message:
|
||||
return None
|
||||
if "main thread of the main interpreter" in message:
|
||||
return None
|
||||
raise
|
||||
|
||||
loop_cls.add_signal_handler = _safe_add_signal_handler
|
||||
return loop_cls, original_add_signal_handler
|
||||
|
||||
|
||||
def _patch_aiohttp_run_app_for_android():
|
||||
try:
|
||||
from aiohttp import web
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
original_run_app = web.run_app
|
||||
|
||||
def _safe_run_app(*args, **kwargs):
|
||||
kwargs.setdefault("handle_signals", False)
|
||||
return original_run_app(*args, **kwargs)
|
||||
|
||||
web.run_app = _safe_run_app
|
||||
return web, original_run_app
|
||||
|
||||
|
||||
def start_server(port=8000, app_files_dir=None):
|
||||
try:
|
||||
storage_dir = None
|
||||
reticulum_config_dir = None
|
||||
if app_files_dir:
|
||||
base_dir = os.path.join(app_files_dir, "meshchatx")
|
||||
storage_dir = os.path.join(base_dir, "storage")
|
||||
reticulum_config_dir = os.path.join(base_dir, "reticulum")
|
||||
os.makedirs(storage_dir, exist_ok=True)
|
||||
os.makedirs(reticulum_config_dir, exist_ok=True)
|
||||
_ensure_android_reticulum_config(reticulum_config_dir)
|
||||
|
||||
original_signal = signal.signal
|
||||
|
||||
def _safe_signal(sig, handler):
|
||||
try:
|
||||
return original_signal(sig, handler)
|
||||
except ValueError as exc:
|
||||
if "main thread of the main interpreter" in str(exc):
|
||||
return None
|
||||
raise
|
||||
|
||||
signal.signal = _safe_signal
|
||||
asyncio_signal_patch = _patch_asyncio_signal_handlers_for_android()
|
||||
aiohttp_run_app_patch = _patch_aiohttp_run_app_for_android()
|
||||
from meshchatx.meshchat import main
|
||||
|
||||
sys.argv = [
|
||||
@@ -13,8 +99,21 @@ def start_server(port=8000):
|
||||
"--port",
|
||||
str(port),
|
||||
]
|
||||
if storage_dir:
|
||||
sys.argv.extend(["--storage-dir", storage_dir])
|
||||
if reticulum_config_dir:
|
||||
sys.argv.extend(["--reticulum-config-dir", reticulum_config_dir])
|
||||
|
||||
main()
|
||||
try:
|
||||
main()
|
||||
finally:
|
||||
signal.signal = original_signal
|
||||
if asyncio_signal_patch is not None:
|
||||
loop_cls, original_add_signal_handler = asyncio_signal_patch
|
||||
loop_cls.add_signal_handler = original_add_signal_handler
|
||||
if aiohttp_run_app_patch is not None:
|
||||
web_module, original_run_app = aiohttp_run_app_patch
|
||||
web_module.run_app = original_run_app
|
||||
except Exception as e:
|
||||
print(f"Error starting MeshChatX server: {e}")
|
||||
import traceback
|
||||
|
||||
|
After Width: | Height: | Size: 68 KiB |
@@ -25,5 +25,39 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loadingText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:background="#CCB00020"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:longClickable="true"
|
||||
android:padding="12dp"
|
||||
android:textIsSelectable="true"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/purple_500"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground_image"/>
|
||||
</adaptive-icon>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/purple_500"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<background android:drawable="@android:color/black"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground_image"/>
|
||||
</adaptive-icon>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -7,7 +7,7 @@
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<item name="android:statusBarColor">@android:color/black</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -Djava.io.tmpdir=.gradle/tmp
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
|
||||
@@ -85,6 +85,14 @@ done
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Keep temporary build artifacts inside project storage to avoid small /tmp limits.
|
||||
GRADLE_TMP_DIR="$APP_HOME/.gradle/tmp"
|
||||
mkdir -p "$GRADLE_TMP_DIR"
|
||||
TMPDIR="$GRADLE_TMP_DIR"
|
||||
TMP="$GRADLE_TMP_DIR"
|
||||
TEMP="$GRADLE_TMP_DIR"
|
||||
export TMPDIR TMP TEMP
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
|
||||