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