mirror of
https://github.com/jwinterm/grim.git
synced 2026-10-20 07:18:32 +02:00
android + ui: update target sdk, add camera support, camera control from native code, camera view, qr scan modal
This commit is contained in:
@@ -12,7 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JByteArray, JObject, JString, JValue};
|
||||
|
||||
use winit::platform::android::activity::AndroidApp;
|
||||
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -26,6 +32,19 @@ impl Android {
|
||||
android_app: app,
|
||||
}
|
||||
}
|
||||
fn call_java_method(&self, name: &str, sig: &str, args: &[JValue]) -> Option<jni::sys::jvalue> {
|
||||
use jni::objects::{JObject};
|
||||
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
let activity = unsafe {
|
||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
||||
};
|
||||
if let Ok(result) = env.call_method(activity, name, sig, args) {
|
||||
return Some(result.as_jni().clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformCallbacks for Android {
|
||||
@@ -33,75 +52,89 @@ impl PlatformCallbacks for Android {
|
||||
// Disable NDK soft input show call before fix for egui.
|
||||
// self.android_app.show_soft_input(false);
|
||||
|
||||
use jni::objects::{JObject};
|
||||
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
let activity = unsafe {
|
||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
||||
};
|
||||
let _ = env.call_method(
|
||||
activity,
|
||||
"showKeyboard",
|
||||
"()V",
|
||||
&[]
|
||||
).unwrap();
|
||||
self.call_java_method("showKeyboard", "()V", &[]).unwrap();
|
||||
}
|
||||
|
||||
fn hide_keyboard(&self) {
|
||||
// Disable NDK soft input hide call before fix for egui.
|
||||
// self.android_app.hide_soft_input(false);
|
||||
|
||||
use jni::objects::{JObject};
|
||||
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
let activity = unsafe {
|
||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
||||
};
|
||||
let _ = env.call_method(
|
||||
activity,
|
||||
"hideKeyboard",
|
||||
"()V",
|
||||
&[]
|
||||
).unwrap();
|
||||
self.call_java_method("hideKeyboard", "()V", &[]).unwrap();
|
||||
}
|
||||
|
||||
fn copy_string_to_buffer(&self, data: String) {
|
||||
use jni::objects::{JObject, JValue};
|
||||
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
let activity = unsafe {
|
||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
||||
};
|
||||
let arg_value = env.new_string(data).unwrap();
|
||||
let _ = env.call_method(
|
||||
activity,
|
||||
"copyText",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]
|
||||
).unwrap();
|
||||
self.call_java_method("copyText",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
|
||||
}
|
||||
|
||||
fn get_string_from_buffer(&self) -> String {
|
||||
use jni::objects::{JObject, JString};
|
||||
|
||||
let result = self.call_java_method("pasteText", "()Ljava/lang/String;", &[]).unwrap();
|
||||
let vm = unsafe { jni::JavaVM::from_raw(self.android_app.vm_as_ptr() as _) }.unwrap();
|
||||
let mut env = vm.attach_current_thread().unwrap();
|
||||
let activity = unsafe {
|
||||
JObject::from_raw(self.android_app.activity_as_ptr() as jni::sys::jobject)
|
||||
};
|
||||
let result = env.call_method(
|
||||
activity,
|
||||
"pasteText",
|
||||
"()Ljava/lang/String;",
|
||||
&[]
|
||||
).unwrap();
|
||||
let j_object: jni::sys::jobject = unsafe { result.as_jni().l };
|
||||
let j_object: jni::sys::jobject = unsafe { result.l };
|
||||
let paste_data: String = unsafe {
|
||||
env.get_string(JString::from(JObject::from_raw(j_object)).as_ref()).unwrap().into()
|
||||
};
|
||||
paste_data
|
||||
}
|
||||
|
||||
fn cameras_amount(&self) -> u32 {
|
||||
let result = self.call_java_method("camerasAmount", "()I", &[]).unwrap();
|
||||
let amount = unsafe { result.i };
|
||||
amount as u32
|
||||
}
|
||||
|
||||
fn switch_camera(&self) {
|
||||
self.call_java_method("switchCamera", "()V", &[]).unwrap();
|
||||
}
|
||||
|
||||
fn start_camera(&self) {
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write().unwrap();
|
||||
*w_image = None;
|
||||
// Start camera.
|
||||
self.call_java_method("startCamera", "()V", &[]).unwrap();
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
// Stop camera.
|
||||
self.call_java_method("stopCamera", "()V", &[]).unwrap();
|
||||
// Clear image.
|
||||
let mut w_image = LAST_CAMERA_IMAGE.write().unwrap();
|
||||
*w_image = None;
|
||||
}
|
||||
|
||||
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
|
||||
let r_image = LAST_CAMERA_IMAGE.read().unwrap();
|
||||
if r_image.is_some() {
|
||||
return Some(r_image.clone().unwrap());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = 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_onCameraImage(
|
||||
env: JNIEnv,
|
||||
_class: JObject,
|
||||
buff: jni::sys::jbyteArray,
|
||||
rotation: jni::sys::jint,
|
||||
) {
|
||||
let arr = unsafe { JByteArray::from_raw(buff) };
|
||||
let image : Vec<u8> = env.convert_byte_array(arr).unwrap();
|
||||
if let Ok(mut w_image) = LAST_CAMERA_IMAGE.write() {
|
||||
*w_image = Some((image, rotation as u32));
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,21 @@ impl PlatformCallbacks for Desktop {
|
||||
let mut clipboard = arboard::Clipboard::new().unwrap();
|
||||
clipboard.get_text().unwrap_or("".to_string())
|
||||
}
|
||||
|
||||
fn camera_image(&self) -> Option<(Vec<u8>, u32)> {
|
||||
Some((vec![].into(), 0))
|
||||
}
|
||||
|
||||
fn cameras_amount(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn start_camera(&self) {
|
||||
}
|
||||
|
||||
fn stop_camera(&self) {
|
||||
}
|
||||
|
||||
fn switch_camera(&self) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,9 @@ pub trait PlatformCallbacks {
|
||||
fn hide_keyboard(&self);
|
||||
fn copy_string_to_buffer(&self, data: String);
|
||||
fn get_string_from_buffer(&self) -> String;
|
||||
fn cameras_amount(&self) -> u32;
|
||||
fn switch_camera(&self);
|
||||
fn start_camera(&self);
|
||||
fn stop_camera(&self);
|
||||
fn camera_image(&self) -> Option<(Vec<u8>, u32)>;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
use egui::load::SizedTexture;
|
||||
use egui::{Pos2, Rect, TextureOptions, Widget};
|
||||
use image::{DynamicImage, EncodableLayout, ImageFormat};
|
||||
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::types::QrScanState;
|
||||
use crate::gui::views::View;
|
||||
|
||||
/// Camera scanner content.
|
||||
pub struct CameraContent {
|
||||
// QR code scanning progress and result.
|
||||
qr_scan_state: Arc<RwLock<QrScanState>>
|
||||
}
|
||||
|
||||
impl Default for CameraContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
qr_scan_state: Arc::new(RwLock::new(QrScanState::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraContent {
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
|
||||
// Draw last image from camera or loader.
|
||||
if let Some(img_data) = cb.camera_image() {
|
||||
// Load image to draw.
|
||||
if let Ok(mut img) =
|
||||
image::load_from_memory_with_format(&*img_data.0, ImageFormat::Jpeg) {
|
||||
// Process image to find QR code.
|
||||
self.scan_qr(&img);
|
||||
// Setup image rotation.
|
||||
img = match img_data.1 {
|
||||
90 => img.rotate90(),
|
||||
180 => img.rotate180(),
|
||||
279 => img.rotate270(),
|
||||
_ => img
|
||||
};
|
||||
// Convert to ColorImage to add at content.
|
||||
let color_image = match &img {
|
||||
DynamicImage::ImageRgb8(image) => {
|
||||
egui::ColorImage::from_rgb(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
other => {
|
||||
let image = other.to_rgba8();
|
||||
egui::ColorImage::from_rgba_unmultiplied(
|
||||
[image.width() as usize, image.height() as usize],
|
||||
image.as_bytes(),
|
||||
)
|
||||
},
|
||||
};
|
||||
// Create image texture.
|
||||
let texture = ui.ctx().load_texture("camera_image",
|
||||
color_image.clone(),
|
||||
TextureOptions::default());
|
||||
let image_size = egui::emath::vec2(color_image.width() as f32,
|
||||
color_image.height() as f32);
|
||||
let sized_image = SizedTexture::new(texture.id(), image_size);
|
||||
// Add image to content.
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::Image::from_texture(sized_image)
|
||||
// Setup to make image cropped at center of square.
|
||||
.uv(Rect::from([Pos2::new(0.125, 0.0), Pos2::new(1.125, 1.0)]))
|
||||
.max_height(ui.available_width())
|
||||
.maintain_aspect_ratio(false)
|
||||
.shrink_to_fit()
|
||||
.ui(ui);
|
||||
});
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
}
|
||||
} else {
|
||||
self.loading_content_ui(ui);
|
||||
}
|
||||
|
||||
// Request redraw.
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
/// Draw camera loading progress content.
|
||||
fn loading_content_ui(&self, ui: &mut egui::Ui) {
|
||||
let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(space);
|
||||
View::big_loading_spinner(ui);
|
||||
ui.add_space(space);
|
||||
});
|
||||
}
|
||||
|
||||
/// Check if image is processing to find QR code.
|
||||
fn image_processing(&self) -> bool {
|
||||
let mut r_scan = self.qr_scan_state.read().unwrap();
|
||||
r_scan.image_processing
|
||||
}
|
||||
|
||||
/// Parse QR code from provided image data.
|
||||
fn scan_qr(&self, data: &DynamicImage) {
|
||||
// Do not scan when another image is processing.
|
||||
if self.image_processing() {
|
||||
return;
|
||||
}
|
||||
// Setup scanning flag.
|
||||
{
|
||||
let mut w_scan = self.qr_scan_state.write().unwrap();
|
||||
w_scan.image_processing = true;
|
||||
}
|
||||
// Launch scanner at separate thread.
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
// Prepare image data.
|
||||
let img = data.to_luma8();
|
||||
let mut img: rqrr::PreparedImage<image::GrayImage>
|
||||
= rqrr::PreparedImage::prepare(img);
|
||||
// Scan and save results.
|
||||
let grids = img.detect_grids();
|
||||
for g in grids {
|
||||
if let Ok((meta, text)) = g.decode() {
|
||||
println!("12345 ecc: {}, text: {}", meta.ecc_level, text.clone());
|
||||
if !text.is_empty() {
|
||||
let mut w_scan = self.qr_scan_state.write().unwrap();
|
||||
w_scan.qr_scan_result = Some(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Setup scanning flag.
|
||||
{
|
||||
let mut w_scan = self.qr_scan_state.write().unwrap();
|
||||
w_scan.image_processing = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Get QR code scan result.
|
||||
pub fn qr_scan_result(&self) -> Option<String> {
|
||||
let r_scan = self.qr_scan_state.read().unwrap();
|
||||
if r_scan.qr_scan_result.is_some() {
|
||||
return Some(r_scan.qr_scan_result.as_ref().unwrap().clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Reset camera content state to default.
|
||||
pub fn clear_state(&mut self) {
|
||||
let mut w_scan = self.qr_scan_state.write().unwrap();
|
||||
*w_scan = QrScanState::default();
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,7 @@ mod network;
|
||||
pub use network::*;
|
||||
|
||||
mod wallets;
|
||||
pub use wallets::*;
|
||||
pub use wallets::*;
|
||||
|
||||
mod camera;
|
||||
pub use camera::*;
|
||||
@@ -136,4 +136,21 @@ impl TextEditOptions {
|
||||
self.scan_qr = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// QR code scanning state.
|
||||
pub struct QrScanState {
|
||||
// Flag to check if image is processing to find QR code.
|
||||
pub(crate) image_processing: bool,
|
||||
// Found QR code content.
|
||||
pub qr_scan_result: Option<String>
|
||||
}
|
||||
|
||||
impl Default for QrScanState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
image_processing: false,
|
||||
qr_scan_result: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,9 +521,12 @@ impl View {
|
||||
});
|
||||
}
|
||||
|
||||
/// Size of big loading spinner.
|
||||
pub const BIG_SPINNER_SIZE: f32 = 104.0;
|
||||
|
||||
/// Draw big gold loading spinner.
|
||||
pub fn big_loading_spinner(ui: &mut egui::Ui) {
|
||||
Spinner::new().size(104.0).color(Colors::GOLD).ui(ui);
|
||||
Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::GOLD).ui(ui);
|
||||
}
|
||||
|
||||
/// Draw small gold loading spinner.
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{BRIDGE, CHAT_CIRCLE_TEXT, CHECK, CHECK_FAT, FOLDER_USER, GEAR_FINE, GRAPH, PACKAGE, PATH, POWER, REPEAT, SCAN, USERS_THREE};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, Root, View};
|
||||
use crate::gui::views::{CameraContent, Modal, Root, View};
|
||||
use crate::gui::views::types::{ModalPosition, TextEditOptions};
|
||||
use crate::gui::views::wallets::{WalletTransactions, WalletMessages, WalletTransport, WalletSettings};
|
||||
use crate::gui::views::wallets::types::{GRIN, WalletTab, WalletTabType};
|
||||
@@ -41,8 +41,13 @@ pub struct WalletContent {
|
||||
/// Flag to check if error occurred during account creation at [`Modal`].
|
||||
account_creation_error: bool,
|
||||
|
||||
/// Camera content for QR scan [`Modal`].
|
||||
camera_content: CameraContent,
|
||||
/// QR code scan result
|
||||
qr_scan_result: Option<String>,
|
||||
|
||||
/// Current tab content to show.
|
||||
pub current_tab: Box<dyn WalletTab>,
|
||||
pub current_tab: Box<dyn WalletTab>
|
||||
}
|
||||
|
||||
impl Default for WalletContent {
|
||||
@@ -52,6 +57,8 @@ impl Default for WalletContent {
|
||||
account_creating: false,
|
||||
account_label_edit: "".to_string(),
|
||||
account_creation_error: false,
|
||||
camera_content: CameraContent::default(),
|
||||
qr_scan_result: None,
|
||||
current_tab: Box::new(WalletTransactions::default())
|
||||
}
|
||||
}
|
||||
@@ -60,6 +67,9 @@ impl Default for WalletContent {
|
||||
/// Identifier for account list [`Modal`].
|
||||
const ACCOUNT_LIST_MODAL: &'static str = "account_list_modal";
|
||||
|
||||
/// Identifier for QR code scan [`Modal`].
|
||||
const QR_CODE_SCAN_MODAL: &'static str = "qr_code_scan_modal";
|
||||
|
||||
impl WalletContent {
|
||||
pub fn ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
@@ -156,6 +166,11 @@ impl WalletContent {
|
||||
self.account_list_modal_ui(ui, wallet, modal, cb);
|
||||
});
|
||||
}
|
||||
QR_CODE_SCAN_MODAL => {
|
||||
Modal::ui(ui.ctx(), |ui, modal| {
|
||||
self.scan_qr_modal_ui(ui, wallet, modal, cb);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +192,15 @@ impl WalletContent {
|
||||
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
|
||||
// Draw button to scan QR code.
|
||||
View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || {
|
||||
//TODO: Scan with QR code.
|
||||
// Load accounts.
|
||||
self.qr_scan_result = None;
|
||||
// Show account list modal.
|
||||
Modal::new(QR_CODE_SCAN_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("scan_qr"))
|
||||
.closeable(false)
|
||||
.show();
|
||||
cb.start_camera();
|
||||
});
|
||||
|
||||
// Draw button to show list of accounts.
|
||||
@@ -336,6 +359,36 @@ impl WalletContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw QR scanner [`Modal`] content.
|
||||
fn scan_qr_modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &mut Wallet,
|
||||
modal: &Modal,
|
||||
cb: &dyn PlatformCallbacks) {
|
||||
ui.add_space(6.0);
|
||||
if let Some(result) = &self.qr_scan_result {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(RichText::new(result).size(16.0).color(Colors::INACTIVE_TEXT));
|
||||
});
|
||||
} else if let Some(result) = self.camera_content.qr_scan_result() {
|
||||
///TODO: parse result and show
|
||||
cb.stop_camera();
|
||||
self.camera_content.clear_state();
|
||||
println!("result: {}", result);
|
||||
self.qr_scan_result = Some(result);
|
||||
} else {
|
||||
self.camera_content.ui(ui, cb);
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("modal.cancel"), Colors::WHITE, || {
|
||||
cb.stop_camera();
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// Draw tab buttons in the bottom of the screen.
|
||||
fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
ui.scope(|ui| {
|
||||
|
||||
@@ -400,7 +400,6 @@ impl WalletTransport {
|
||||
r_success.clone()
|
||||
}
|
||||
|
||||
/// Draw amount input [`Modal`] content to send over Tor.
|
||||
/// Draw amount input [`Modal`] content to send over Tor.
|
||||
fn send_tor_modal_ui(&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
|
||||
Reference in New Issue
Block a user