diff --git a/android/app/build.gradle b/android/app/build.gradle
index 35bde38..ce1724f 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -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"
}
}
}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000..bc707fd
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -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.**
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fd5f5ef..f2955e7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -8,13 +8,19 @@
+
+
+
+
+
+
-
diff --git a/android/app/src/main/java/com/meshchatx/MainActivity.java b/android/app/src/main/java/com/meshchatx/MainActivity.java
index 887bbfd..67f5da3 100644
--- a/android/app/src/main/java/com/meshchatx/MainActivity.java
+++ b/android/app/src/main/java/com/meshchatx/MainActivity.java
@@ -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 filePathCallback = null;
+ private boolean startupPageLoaded = false;
+ private boolean backendFailed = false;
+ private int connectionAttempts = 0;
+ private final ActivityResultLauncher 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 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 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 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();
}
diff --git a/android/app/src/main/python/meshchat_wrapper.py b/android/app/src/main/python/meshchat_wrapper.py
index de627f7..081c3c7 100644
--- a/android/app/src/main/python/meshchat_wrapper.py
+++ b/android/app/src/main/python/meshchat_wrapper.py
@@ -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
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground_image.png b/android/app/src/main/res/drawable/ic_launcher_foreground_image.png
new file mode 100644
index 0000000..5e4fad8
Binary files /dev/null and b/android/app/src/main/res/drawable/ic_launcher_foreground_image.png differ
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
index 8fc7bca..59ec676 100644
--- a/android/app/src/main/res/layout/activity_main.xml
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -25,5 +25,39 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 4f0f28e..ea79098 100644
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 4f0f28e..ea79098 100644
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a5e5afa
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a5e5afa
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..82ca161
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..82ca161
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..af92a45
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..af92a45
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..e9c5aaf
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e9c5aaf
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c7c02d2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c7c02d2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
index a0ae9bd..48af0b8 100644
--- a/android/app/src/main/res/values/themes.xml
+++ b/android/app/src/main/res/values/themes.xml
@@ -7,7 +7,7 @@
- @color/teal_200
- @color/teal_700
- @color/black
- - ?attr/colorPrimaryVariant
+ - @android:color/black
diff --git a/android/gradle.properties b/android/gradle.properties
index fdea6eb..269eb37 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -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
diff --git a/android/gradlew b/android/gradlew
index 8b7901c..7145c99 100755
--- a/android/gradlew
+++ b/android/gradlew
@@ -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