From ed2dc880aa202246e039c4bcf989430f92774898 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Fri, 21 Nov 2025 01:34:51 +0300 Subject: [PATCH] android: migrate back to gameactivity, update to API 36, fix back navigation at network panel --- Cargo.lock | 46 ++-- Cargo.toml | 10 +- android/app/build.gradle | 29 ++- android/app/src/main/AndroidManifest.xml | 40 +-- .../mw/gri/android/BackgroundService.java | 20 +- .../java/mw/gri/android/MainActivity.java | 231 ++++++++++++------ .../src/main/java/mw/gri/android/Utils.java | 2 +- android/build.gradle | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- scripts/android.sh | 2 +- src/gui/app.rs | 35 ++- src/gui/views/content.rs | 10 +- src/gui/views/input/edit.rs | 100 +++++++- src/gui/views/network/content.rs | 10 + src/gui/views/wallets/content.rs | 13 +- src/gui/views/wallets/wallet/content.rs | 7 - src/lib.rs | 2 +- 17 files changed, 370 insertions(+), 193 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4826f80f..a41aa596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1498,7 +1498,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ - "unicode-width 0.2.2", + "unicode-width", ] [[package]] @@ -2392,9 +2392,9 @@ dependencies = [ [[package]] name = "ecolor" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf31f99fad93fe83c1055b92b5c1b135f1ecfa464789817c372000e768d4bd1" +checksum = "084980ebede2fb1ad6c4f54285b3e489052ef2b6aa4016e4c19349417adc75c5" dependencies = [ "bytemuck", "emath", @@ -2463,9 +2463,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b829d302a09deb4acde242262a1840ba14fadd0371980ebf713060077a1987bc" +checksum = "4f60ee3c69cd01d5725b4ef476ed6a3a6009968c57843a2f1beb96f021e4a54b" dependencies = [ "ahash", "bytemuck", @@ -2498,9 +2498,9 @@ dependencies = [ [[package]] name = "egui" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9b5d3376c79439f53a78bf7da1e3c0b862ffa3e29f46ab0f3e107430f2e576" +checksum = "b75645894de4ca1695ab3ab7201c7953bb95c1725aafeefa6822dc901ad2a81b" dependencies = [ "ahash", "bitflags 2.10.0", @@ -2515,9 +2515,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef1fe83ba30b3d045814b2d811804f2a7e50a832034c975408f71c20df596e4" +checksum = "acdaac35a6e0ff458aaa5776b2bda578e38334a9d1258f7795042a70372745b7" dependencies = [ "ahash", "bytemuck", @@ -2535,9 +2535,9 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ea8cb063c00d8f23ce11279c01eb63a195a72be0e21d429148246dab7983e" +checksum = "4772ed5f16fa8ec2ba295e58f62b58ee83fcf49e67ec13d2b7ddf4e9a2dea34e" dependencies = [ "arboard", "bytemuck", @@ -2556,9 +2556,9 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdced1964ad8a02a116b1307f7b4f73dbe408c5f53dcdd488f527609f261da60" +checksum = "550e844e608e356f4ad6843c510aa9bb5838b427e4700ed0056e9746ceeed866" dependencies = [ "ahash", "egui", @@ -2572,9 +2572,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668c0d4f726cc33838f0915f6b8c00af0ca0910e975ab58cf34b3e39c614552c" +checksum = "17b94ff67a1d18933fff2519f5f57c388f932c093036c381fb9ae2853b3e1e09" dependencies = [ "bytemuck", "egui", @@ -2614,9 +2614,9 @@ dependencies = [ [[package]] name = "emath" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c615516cdceec867065f20d7db13d8eb8aedd65c9e32cc0c7c379380fa42e6e8" +checksum = "0e561352ae95c22ad179fb56c38d6e6eecd86cf4925cf5c70e738dd01df9b620" dependencies = [ "bytemuck", ] @@ -2742,9 +2742,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.33.0" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9926b9500ccb917adb070207ec722dd8ea78b8321f94a85ebec776f501f2930c" +checksum = "a909ce8cee63e8350fb0c251ad39277a5b24f19add60787e84a3b3ab3f2bd83a" dependencies = [ "ab_glyph", "ahash", @@ -7206,7 +7206,7 @@ dependencies = [ "is-terminal", "lazy_static", "term 0.7.0", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -11124,12 +11124,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "unicode-xid" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c30e7f64..4f7fe338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,8 +151,8 @@ nokhwa-mac = { git = "https://code.gri.mw/ardocrat/nokhwa", rev = "612c861ef153c [target.'cfg(not(target_os = "android"))'.dependencies] env_logger = "0.11.3" -winit = { version = "0.30.11" } -eframe = { version = "0.33.0", default-features = false, features = ["glow", "wayland"] } +winit = { version = "0.30.12" } +eframe = { version = "0.33.2", default-features = false, features = ["glow", "wayland"] } arboard = "3.2.0" rfd = "0.15.0" interprocess = { version = "2.2.1", features = ["tokio"] } @@ -160,6 +160,6 @@ interprocess = { version = "2.2.1", features = ["tokio"] } [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.15.0" jni = "0.21.1" -android-activity = { version = "0.6.0", features = ["native-activity"] } -winit = { version = "0.30.11", features = ["android-native-activity"] } -eframe = { version = "0.33.0", default-features = false, features = ["glow", "android-native-activity"] } +android-activity = { version = "0.6.0", features = ["game-activity"] } +winit = { version = "0.30.12", features = ["android-game-activity"] } +eframe = { version = "0.33.2", default-features = false, features = ["glow", "android-game-activity"] } \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index e90d5603..fe5641a6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,15 +3,19 @@ plugins { } android { - compileSdk 35 + compileSdk 36 ndkVersion '26.0.10792818' defaultConfig { applicationId "mw.gri.android" minSdk 24 - targetSdk 35 - versionCode 4 - versionName "0.2.4" + targetSdk 36 + versionCode 5 + versionName "0.3.0" + } + + lint { + checkReleaseBuilds false } def keystorePropertiesFile = rootProject.file("keystore.properties") @@ -27,6 +31,7 @@ android { storePassword keystoreProperties['storePassword'] } } + } buildTypes { @@ -46,8 +51,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } namespace 'mw.gri.android' } @@ -55,9 +60,11 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' - // Android Camera - implementation 'androidx.camera:camera-core:1.4.2' - implementation 'androidx.camera:camera-camera2:1.4.2' - implementation 'androidx.camera:camera-lifecycle:1.4.2' -} + // To use the Games Activity library + implementation "androidx.games:games-activity:2.0.2" + // Android Camera + implementation 'androidx.camera:camera-core:1.5.1' + implementation 'androidx.camera:camera-camera2:1.5.1' + implementation 'androidx.camera:camera-lifecycle:1.5.1' +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1468730c..f04a90c7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -15,31 +14,33 @@ + android:hardwareAccelerated="true" + android:largeHeap="true" + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="Grim" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/Theme.Main" + android:enableOnBackInvokedCallback="false" + tools:ignore="UnusedAttribute"> + android:name=".FileProvider" + android:authorities="mw.gri.android.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/paths" /> + android:launchMode="singleTask" + android:name=".MainActivity" + android:configChanges="orientation|screenSize" + android:exported="true"> @@ -52,6 +53,7 @@ + diff --git a/android/app/src/main/java/mw/gri/android/BackgroundService.java b/android/app/src/main/java/mw/gri/android/BackgroundService.java index 636df813..f0df3b11 100644 --- a/android/app/src/main/java/mw/gri/android/BackgroundService.java +++ b/android/app/src/main/java/mw/gri/android/BackgroundService.java @@ -16,7 +16,7 @@ import static android.app.Notification.EXTRA_NOTIFICATION_ID; public class BackgroundService extends Service { private static final String TAG = BackgroundService.class.getSimpleName(); - + private PowerManager.WakeLock mWakeLock; private final Handler mHandler = new Handler(Looper.getMainLooper()); @@ -31,7 +31,6 @@ public class BackgroundService extends Service { public static final String ACTION_START_NODE = "start_node"; public static final String ACTION_STOP_NODE = "stop_node"; - public static final String ACTION_EXIT = "exit"; private final Runnable mUpdateSyncStatus = new Runnable() { @SuppressLint("RestrictedApi") @@ -82,18 +81,6 @@ public class BackgroundService extends Service { .getBroadcast(BackgroundService.this, 1, startStopIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); mNotificationBuilder.addAction(R.drawable.ic_stop, getStopText(), i); } - - // Set up a button to exit from the app. - if (canStart || canStop) { - Intent exitIntent = new Intent(BackgroundService.this, NotificationActionsReceiver.class); - if (Build.VERSION.SDK_INT > 25) { - exitIntent.putExtra(EXTRA_NOTIFICATION_ID, NOTIFICATION_ID); - } - exitIntent.setAction(ACTION_EXIT); - PendingIntent i = PendingIntent - .getBroadcast(BackgroundService.this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); - mNotificationBuilder.addAction(R.drawable.ic_close, getExitText(), i); - } } // Update notification. @@ -247,9 +234,6 @@ public class BackgroundService extends Service { // Check if stop node is possible. private native boolean canStopNode(); - // Get exit text for notification. - private native String getExitText(); - // Check if app from the app is needed after node stop. private native boolean exitAppAfterNodeStop(); -} +} \ No newline at end of file diff --git a/android/app/src/main/java/mw/gri/android/MainActivity.java b/android/app/src/main/java/mw/gri/android/MainActivity.java index 5fc98ae4..abbfe1de 100644 --- a/android/app/src/main/java/mw/gri/android/MainActivity.java +++ b/android/app/src/main/java/mw/gri/android/MainActivity.java @@ -3,7 +3,6 @@ package mw.gri.android; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.NativeActivity; import android.content.*; import android.content.pm.PackageManager; import android.content.res.Configuration; @@ -13,19 +12,26 @@ import android.os.Process; import android.provider.Settings; import android.system.ErrnoException; import android.system.Os; +import android.util.Log; +import android.util.Size; +import android.view.KeyEvent; import android.view.View; -import android.view.inputmethod.InputMethodManager; +import android.window.OnBackInvokedDispatcher; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.camera.core.*; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.graphics.Insets; +import androidx.core.os.BuildCompat; import androidx.core.view.DisplayCutoutCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; - +import com.google.androidgamesdk.GameActivity; +import com.google.androidgamesdk.gametextinput.State; import com.google.common.util.concurrent.ListenableFuture; import java.io.*; @@ -36,30 +42,27 @@ import java.util.concurrent.Executors; import static android.content.ClipDescription.MIMETYPE_TEXT_HTML; import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; -public class MainActivity extends NativeActivity { - private static final int FILE_PICK_REQUEST = 1001; - private static final int FILE_PERMISSIONS_REQUEST = 1002; +public class MainActivity extends GameActivity { + public static String STOP_APP_ACTION = "STOP_APP"; private static final int NOTIFICATIONS_PERMISSION_CODE = 1; private static final int CAMERA_PERMISSION_CODE = 2; - public static final String STOP_APP_ACTION = "STOP_APP_ACTION"; - static { System.loadLibrary("grim"); } - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @SuppressLint("RestrictedApi") + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override - public void onReceive(Context context, Intent intent) { - if (Objects.equals(intent.getAction(), MainActivity.STOP_APP_ACTION)) { + public void onReceive(Context ctx, Intent i) { + if (Objects.equals(i.getAction(), STOP_APP_ACTION)) { exit(); } } }; private final ImageAnalysis mImageAnalysis = new ImageAnalysis.Builder() + .setTargetResolution(new Size(640, 480)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build(); @@ -68,6 +71,9 @@ public class MainActivity extends NativeActivity { private ExecutorService mCameraExecutor = null; private boolean mUseBackCamera = true; + private ActivityResultLauncher mFilePickResult = null; + private ActivityResultLauncher mOpenFilePermissionsResult = null; + @SuppressLint("UnspecifiedRegisterReceiverFlag") @Override protected void onCreate(Bundle savedInstanceState) { @@ -79,15 +85,14 @@ public class MainActivity extends NativeActivity { } // Clear cache on start. - String cacheDir = Objects.requireNonNull(getExternalCacheDir()).getPath(); - if (savedInstanceState == null) { - Utils.deleteDirectoryContent(new File(cacheDir), false); + if (savedInstanceState == null && getExternalCacheDir() != null) { + Utils.deleteDirectoryContent(new File(getExternalCacheDir().getPath()), false); } // Setup environment variables for native code. try { Os.setenv("HOME", Objects.requireNonNull(getExternalFilesDir("")).getPath(), true); - Os.setenv("XDG_CACHE_HOME", cacheDir, true); + Os.setenv("XDG_CACHE_HOME", Objects.requireNonNull(getExternalCacheDir()).getPath(), true); Os.setenv("ARTI_FS_DISABLE_PERMISSION_CHECKS", "true", true); } catch (ErrnoException e) { throw new RuntimeException(e); @@ -95,10 +100,56 @@ public class MainActivity extends NativeActivity { super.onCreate(null); - ContextCompat.registerReceiver(this, mReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED); + // Register receiver to finish activity from the BackgroundService. + ContextCompat.registerReceiver(this, mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION), ContextCompat.RECEIVER_NOT_EXPORTED); + + // Register associated file opening result. + mOpenFilePermissionsResult = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (Build.VERSION.SDK_INT >= 30) { + if (Environment.isExternalStorageManager()) { + onFile(); + } + } else if (result.getResultCode() == RESULT_OK) { + onFile(); + } + } + ); + // Register file pick result. + mFilePickResult = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + int resultCode = result.getResultCode(); + Intent data = result.getData(); + if (resultCode == Activity.RESULT_OK) { + String path = ""; + if (data != null && data.getData() != null) { + Uri uri = data.getData(); + String name = "pick" + Utils.getFileExtension(uri, this); + File file = new File(getExternalCacheDir(), name); + try (InputStream is = getContentResolver().openInputStream(uri); + OutputStream os = new FileOutputStream(file)) { + byte[] buffer = new byte[1024]; + int length; + while (true) { + assert is != null; + if (!((length = is.read(buffer)) > 0)) break; + os.write(buffer, 0, length); + } + } catch (Exception e) { + Log.e("grim", e.toString()); + } + path = file.getPath(); + } + onFilePick(path); + } else { + onFilePick(""); + } + }); // Listener for display insets (cutouts) to pass values into native code. - View content = findViewById(android.R.id.content).getRootView(); + View content = getWindow().getDecorView().findViewById(android.R.id.content); ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> { // Get display cutouts. DisplayCutoutCompat dc = insets.getDisplayCutout(); @@ -127,7 +178,7 @@ public class MainActivity extends NativeActivity { return insets; }); - content.post(() -> { + findViewById(android.R.id.content).post(() -> { // Request notifications permissions if needed. if (Build.VERSION.SDK_INT >= 33) { String notificationsPermission = Manifest.permission.POST_NOTIFICATIONS; @@ -149,43 +200,8 @@ public class MainActivity extends NativeActivity { } } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - switch (requestCode) { - case FILE_PICK_REQUEST: - if (Build.VERSION.SDK_INT >= 30) { - if (Environment.isExternalStorageManager()) { - onFile(); - } - } else if (resultCode == RESULT_OK) { - onFile(); - } - case FILE_PERMISSIONS_REQUEST: - if (resultCode == Activity.RESULT_OK) { - String path = ""; - if (data != null) { - Uri uri = data.getData(); - String name = "pick" + Utils.getFileExtension(uri, this); - File file = new File(getExternalCacheDir(), name); - try (InputStream is = getContentResolver().openInputStream(uri); - OutputStream os = new FileOutputStream(file)) { - byte[] buffer = new byte[1024]; - int length; - while ((length = is.read(buffer)) > 0) { - os.write(buffer, 0, length); - } - } catch (Exception e) { - e.printStackTrace(); - } - path = file.getPath(); - } - onFilePick(path); - } else { - onFilePick(""); - } - } - } + // Pass display insets into native code. + public native void onDisplayInsets(int[] cutouts); @Override protected void onNewIntent(Intent intent) { @@ -209,12 +225,13 @@ public class MainActivity extends NativeActivity { if (Build.VERSION.SDK_INT >= 30) { if (!Environment.isExternalStorageManager()) { Intent i = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - startActivityForResult(i, FILE_PERMISSIONS_REQUEST); + mOpenFilePermissionsResult.launch(i); return; } } try { ParcelFileDescriptor parcelFile = getContentResolver().openFileDescriptor(data, "r"); + assert parcelFile != null; FileReader fileReader = new FileReader(parcelFile.getFileDescriptor()); BufferedReader reader = new BufferedReader(fileReader); String line; @@ -228,7 +245,7 @@ public class MainActivity extends NativeActivity { // Provide file content into native code. onData(buff.toString()); } catch (Exception e) { - e.printStackTrace(); + Log.e("grim", e.toString()); } } @@ -251,20 +268,87 @@ public class MainActivity extends NativeActivity { if (results.length != 0 && results[0] == PackageManager.PERMISSION_GRANTED) { switch (requestCode) { case NOTIFICATIONS_PERMISSION_CODE: { - // Start notification service. BackgroundService.start(this); return; } case CAMERA_PERMISSION_CODE: { - // Start camera. startCamera(); } } } } - // Implemented into native code to handle display insets change. - native void onDisplayInsets(int[] cutouts); + @Override + protected void onTextInputEventNative(long l, State state) { + super.onTextInputEventNative(l, state); + if (state.selectionEnd > state.composingRegionStart && state.composingRegionStart >= 0) { + String input = String.valueOf(state.text.charAt(state.composingRegionStart)); + if (input.contains("\n")) { + onEnterInput(); + } else { + onTextInput(input); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + onBack(); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { + onClearInput(); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + onEnterInput(); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_0) { + onTextInput("0"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_1) { + onTextInput("1"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_2) { + onTextInput("2"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_3) { + onTextInput("3"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_4) { + onTextInput("4"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_5) { + onTextInput("5"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_6) { + onTextInput("6"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_7) { + onTextInput("7"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_8) { + onTextInput("8"); + return false; + } else if (event.getKeyCode() == KeyEvent.KEYCODE_9) { + onTextInput("9"); + return false; + } + } + return super.dispatchKeyEvent(event); + } + + // Pass back navigation event into native code. + public native void onBack(); + + // Pass clear key event into native code. + public native void onClearInput(); + + // Pass enter key event into native code. + public native void onEnterInput(); + + // Pass last entered character from soft keyboard into native code. + public native void onTextInput(String character); // Called from native code to exit app. public void exit() { @@ -273,6 +357,7 @@ public class MainActivity extends NativeActivity { @Override protected void onDestroy() { + unregisterReceiver(mBroadcastReceiver); BackgroundService.stop(this); // Kill process after 3 secs if app was terminated from recent apps to prevent app hang. @@ -317,18 +402,6 @@ public class MainActivity extends NativeActivity { return text; } - // Called from native code to show keyboard. - public void showKeyboard() { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(getWindow().getDecorView(), InputMethodManager.SHOW_IMPLICIT); - } - - // Called from native code to hide keyboard. - public void hideKeyboard() { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0); - } - // Called from native code to start camera. public void startCamera() { String notificationsPermission = Manifest.permission.CAMERA; @@ -379,7 +452,7 @@ public class MainActivity extends NativeActivity { } // Apply declared configs to CameraX using the same lifecycle owner mCameraProvider.unbindAll(); -// mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis); + mCameraProvider.bindToLifecycle(this, cameraSelector, mImageAnalysis); } // Called from native code to stop camera. @@ -418,7 +491,7 @@ public class MainActivity extends NativeActivity { Uri uri = FileProvider.getUriForFile(this, "mw.gri.android.fileprovider", file); Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_STREAM, uri); - intent.setType("*/*"); + intent.setType("text/*"); startActivity(Intent.createChooser(intent, "Share data")); } @@ -431,10 +504,10 @@ public class MainActivity extends NativeActivity { // Called from native code to pick the file. public void pickFile() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); + intent.setType("text/*"); try { - startActivityForResult(Intent.createChooser(intent, "Pick file"), FILE_PICK_REQUEST); - } catch (ActivityNotFoundException ex) { + mFilePickResult.launch(Intent.createChooser(intent, "Pick file")); + } catch (android.content.ActivityNotFoundException ex) { onFilePick(""); } } diff --git a/android/app/src/main/java/mw/gri/android/Utils.java b/android/app/src/main/java/mw/gri/android/Utils.java index 1a610bbb..92617c04 100644 --- a/android/app/src/main/java/mw/gri/android/Utils.java +++ b/android/app/src/main/java/mw/gri/android/Utils.java @@ -153,4 +153,4 @@ public class Utils { String fileType = context.getContentResolver().getType(uri); return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType); } -} +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 56ef74f1..7346e0dc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.10.0' apply false - id 'com.android.library' version '8.10.0' apply false + id 'com.android.application' version '8.13.1' apply false + id 'com.android.library' version '8.13.1' apply false } \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 0e712cce..e30fe896 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon May 02 15:39:12 BST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://code.gri.mw/ardocrat/gradle/releases/download/v8.11.1/gradle-8.11.1-bin.zip +distributionUrl=https\://code.gri.mw/ardocrat/gradle/releases/download/v8.13/gradle-8.13-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/scripts/android.sh b/scripts/android.sh index e18330f5..e7ec91c9 100755 --- a/scripts/android.sh +++ b/scripts/android.sh @@ -82,7 +82,7 @@ function build_apk() { done else # Get version - version=v$2 + version=$2 if [[ -z "$version" ]]; then version=v$(grep -m 1 -Po 'version = "\K[^"]*' ../Cargo.toml) fi diff --git a/src/gui/app.rs b/src/gui/app.rs index 07277c63..040af330 100755 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::atomic::{AtomicBool, Ordering}; use egui::epaint::RectShape; use egui::{Align, Context, CornerRadius, CursorIcon, LayerId, Layout, Modifiers, Order, ResizeDirection, Stroke, StrokeKind, UiBuilder, ViewportCommand}; +use lazy_static::lazy_static; use crate::gui::icons::{ARROWS_IN, ARROWS_OUT, CARET_DOWN, MOON, SUN, X}; use crate::gui::platform::PlatformCallbacks; @@ -22,6 +24,11 @@ use crate::gui::views::{Content, KeyboardContent, Modal, TitlePanel, View}; use crate::gui::Colors; use crate::AppConfig; +lazy_static! { + /// State to check if platform Back button was pressed. + static ref BACK_BUTTON_PRESSED: AtomicBool = AtomicBool::new(false); +} + /// Implements ui entry point and contains platform-specific callbacks. pub struct App { /// Handles platform-specific functionality. @@ -63,10 +70,15 @@ impl App { self.first_draw = false; } - // Handle Esc keyboard key event. - if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape) || + // Handle Esc keyboard key event and platform Back button key event. + let back_pressed = BACK_BUTTON_PRESSED.load(Ordering::Relaxed); + if back_pressed || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Escape) || i.consume_key(Modifiers::NONE, egui::Key::BrowserBack)) { - self.content.on_back(&self.platform); + // Pass event to content. + self.content.on_back(ctx, &self.platform); + if back_pressed { + BACK_BUTTON_PRESSED.store(false, Ordering::Relaxed); + } // Request repaint to update previous content. ctx.request_repaint(); } @@ -134,7 +146,9 @@ impl App { false }; if keyboard_showing { - ctx.move_to_top(LayerId::new(Order::Middle, egui::Id::new(KeyboardContent::WINDOW_ID))); + ctx.move_to_top( + LayerId::new(Order::Middle, egui::Id::new(KeyboardContent::WINDOW_ID)) + ); } } // Reset keyboard state for newly opened modal. @@ -397,4 +411,17 @@ impl eframe::App for App { } Colors::TRANSPARENT.to_normalized_gamma_f32() } +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Handle Back key code event from Android. +pub extern "C" fn Java_mw_gri_android_MainActivity_onBack( + _env: jni::JNIEnv, + _class: jni::objects::JObject, + _activity: jni::objects::JObject, +) { + BACK_BUTTON_PRESSED.store(true, Ordering::Relaxed); } \ No newline at end of file diff --git a/src/gui/views/content.rs b/src/gui/views/content.rs index f157b21e..827ab3f7 100644 --- a/src/gui/views/content.rs +++ b/src/gui/views/content.rs @@ -146,9 +146,15 @@ impl Content { pub const EXIT_CONFIRMATION_MODAL: &'static str = "exit_confirmation_modal"; /// Called to navigate back, return `true` if action was not consumed. - pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool { + pub fn on_back(&mut self, ctx: &egui::Context, cb: &dyn PlatformCallbacks) -> bool { if Modal::on_back() { - if self.wallets.on_back(cb) { + let dual_panel = Self::is_dual_panel_mode(ctx); + if !dual_panel && Self::is_network_panel_open() { + if self.network.on_back() { + Self::toggle_network_panel(); + return false; + } + } else if self.wallets.on_back(cb) { Self::show_exit_modal(); return false; } diff --git a/src/gui/views/input/edit.rs b/src/gui/views/input/edit.rs index 645a992e..94639dde 100644 --- a/src/gui/views/input/edit.rs +++ b/src/gui/views/input/edit.rs @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Layout, TextBuffer, TextStyle, Widget, Align}; +use std::sync::Arc; +use egui::{Layout, TextBuffer, TextStyle, Widget, Align, ViewportCommand}; use egui::text_edit::TextEditState; +use lazy_static::lazy_static; +use parking_lot::RwLock; + use crate::gui::Colors; use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN}; use crate::gui::platform::PlatformCallbacks; @@ -24,11 +28,11 @@ use crate::gui::views::{KeyboardEvent, View}; pub struct TextEdit { /// View identifier. id: egui::Id, - /// Check if horizontal centering is needed. + /// Horizontal text centering is needed. h_center: bool, - /// Check if focus is needed. + /// Focus is needed. focus: bool, - /// Check if focus request was passed. + /// Focus request was passed. focus_request: bool, /// Hide letters and draw button to show/hide letters. password: bool, @@ -38,9 +42,9 @@ pub struct TextEdit { paste: bool, /// Show button to scan QR code into text. scan_qr: bool, - /// Callback when scan button was pressed. + /// Scan button was pressed. pub scan_pressed: bool, - /// Callback when Enter key was pressed. + /// Tab or Enter keys were pressed to focus on next line. pub enter_pressed: bool, /// Flag to enter only numbers. numeric: bool, @@ -65,7 +69,7 @@ impl TextEdit { scan_pressed: false, enter_pressed: false, numeric: false, - no_soft_keyboard: false, + no_soft_keyboard: is_android(), } } @@ -151,6 +155,7 @@ impl TextEdit { // Reset keyboard state for newly focused. if clicked || self.focus_request { + ui.ctx().send_viewport_cmd(ViewportCommand::IMEAllowed(true)); KeyboardContent::reset_window_state(); } @@ -160,9 +165,10 @@ impl TextEdit { data.insert_temp(focused_input_id, self.id); }); self.enter_pressed = self.on_soft_input(ui, self.id, false, input); - // Check Enter key input. + // Check Enter or Tab keys press. if !self.focus_request { - if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter)) { + if ui.ctx().input(|i| i.key_pressed(egui::Key::Enter) || + i.key_pressed(egui::Key::Tab)) { self.enter_pressed = true; } } @@ -175,12 +181,24 @@ impl TextEdit { } }); }); + // Repaint on Android to handle input from Java code without delays. + if is_android() { + ui.ctx().request_repaint(); + } } /// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed. fn on_soft_input(&self, ui: &mut egui::Ui, id: egui::Id, multiline: bool, value: &mut String) -> bool { - if let Some(input) = KeyboardContent::consume_event() { + let event: Option = if is_android() { + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + w_input.take() + } else { + KeyboardContent::consume_event() + }; + + // Handle keyboard input event. + if let Some(e) = event { let mut enter_pressed = false; let mut state = TextEditState::load(ui.ctx(), id).unwrap(); match state.cursor.char_range() { @@ -194,7 +212,7 @@ impl TextEdit { r.secondary.index as f32) as usize; let end_select = f32::max(r.primary.index as f32, r.secondary.index as f32) as usize; - match input { + match e { KeyboardEvent::TEXT(text) => { if selected { *value = { @@ -318,4 +336,64 @@ impl TextEdit { self.no_soft_keyboard = true; self } +} + +/// Check if current system is Android. +fn is_android() -> bool { + egui::os::OperatingSystem::from_target_os() == egui::os::OperatingSystem::Android +} + +lazy_static! { + static ref LAST_SOFT_KEYBOARD_EVENT: Arc>> = Arc::new(RwLock::new(None)); +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Callback from Java code with last entered character from soft keyboard. +pub extern "C" fn Java_mw_gri_android_MainActivity_onTextInput( + _env: jni::JNIEnv, + _class: jni::objects::JObject, + char: jni::sys::jstring +) { + use jni::objects::JString; + + unsafe { + let j_obj = JString::from_raw(char); + let j_str = _env.get_string_unchecked(j_obj.as_ref()).unwrap(); + match j_str.to_str() { + Ok(str) => { + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + *w_input = Some(KeyboardEvent::TEXT(str.to_string())); + } + Err(_) => {} + } + } +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Callback from Java code when Clear key was pressed at soft keyboard. +pub extern "C" fn Java_mw_gri_android_MainActivity_onClearInput( + _env: jni::JNIEnv, + _class: jni::objects::JObject, +) { + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + *w_input = Some(KeyboardEvent::CLEAR); +} + +#[allow(dead_code)] +#[cfg(target_os = "android")] +#[allow(non_snake_case)] +#[no_mangle] +/// Callback from Java code when Enter key was pressed at soft keyboard. +pub extern "C" fn Java_mw_gri_android_MainActivity_onEnterInput( + _env: jni::JNIEnv, + _class: jni::objects::JObject, +) { + let mut w_input = LAST_SOFT_KEYBOARD_EVENT.write(); + *w_input = Some(KeyboardEvent::ENTER); } \ No newline at end of file diff --git a/src/gui/views/network/content.rs b/src/gui/views/network/content.rs index 7701823f..7c114649 100644 --- a/src/gui/views/network/content.rs +++ b/src/gui/views/network/content.rs @@ -213,6 +213,16 @@ impl NetworkContent { } } + /// Navigate back, return `true` if action was not consumed. + pub fn on_back(&mut self) -> bool { + if self.showing_settings() { + // Close settings. + self.settings_content = None; + return false; + } + true + } + /// Check if application settings content is showing. pub fn showing_settings(&self) -> bool { self.settings_content.is_some() diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 3dea6b2d..da6949e3 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::time::Duration; use egui::scroll_area::ScrollBarVisibility; use egui::{Align, CornerRadius, Id, Layout, Margin, RichText, ScrollArea, StrokeKind}; -use std::time::Duration; - +use egui::os::OperatingSystem; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::SettingsContent; @@ -130,6 +130,12 @@ impl ContentContainer for WalletsContent { } fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + ui.ctx().request_repaint_after(if OperatingSystem::from_target_os() == OperatingSystem::Android { + Duration::from_millis(100) + } else { + Duration::from_millis(1000) + }); + if let Some(data) = crate::consume_incoming_data() { if !data.is_empty() { self.on_data(ui, Some(data), cb); @@ -221,9 +227,6 @@ impl ContentContainer for WalletsContent { ..Default::default() }) .show_animated_inside(ui, !list_hidden, |ui| { - if !dual_panel && !showing_wallet { - ui.ctx().request_repaint_after(Duration::from_millis(1000)); - } // Show wallet list. self.wallet_list_ui(ui, cb); }); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 5143ba5d..18e8037b 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -15,7 +15,6 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, Margin, RichText, ScrollArea}; use grin_chain::SyncStatus; -use std::time::Duration; use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK}; use crate::gui::platform::PlatformCallbacks; @@ -83,12 +82,6 @@ impl WalletContentContainer for WalletContent { } fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - if self.account_content.can_back() { - ui.ctx().request_repaint(); - } else { - ui.ctx().request_repaint_after(Duration::from_millis(1000)); - } - let dual_panel = Content::is_dual_panel_mode(ui.ctx()); let show_wallets_dual = AppConfig::show_wallets_at_dual_panel(); diff --git a/src/lib.rs b/src/lib.rs index bb4a95a0..be717885 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ fn android_main(app: AndroidApp) { { std::env::set_var("RUST_BACKTRACE", "full"); let log_config = android_logger::Config::default() - .with_max_level(log::LevelFilter::Info) + .with_max_level(log::LevelFilter::Debug) .with_tag("grim"); android_logger::init_once(log_config); }