android: pick file
This commit is contained in:
parent
2fb3bffc8f
commit
d16624d423
7 changed files with 200 additions and 73 deletions
|
@ -1,6 +1,8 @@
|
|||
package mw.gri.android;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.*;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
|
@ -14,6 +16,8 @@ import android.util.Size;
|
|||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.*;
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider;
|
||||
|
@ -26,7 +30,7 @@ import androidx.core.view.WindowInsetsCompat;
|
|||
import com.google.androidgamesdk.GameActivity;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
|
@ -63,6 +67,9 @@ public class MainActivity extends GameActivity {
|
|||
private ExecutorService mCameraExecutor = null;
|
||||
private boolean mUseBackCamera = true;
|
||||
|
||||
private ActivityResultLauncher<Intent> mFilePickResultLauncher = null;
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Clear cache on start.
|
||||
|
@ -84,6 +91,36 @@ public class MainActivity extends GameActivity {
|
|||
// Register receiver to finish activity from the BackgroundService.
|
||||
registerReceiver(mBroadcastReceiver, new IntentFilter(STOP_APP_ACTION));
|
||||
|
||||
// Register file pick result launcher.
|
||||
mFilePickResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
int resultCode = result.getResultCode();
|
||||
Intent data = result.getData();
|
||||
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("");
|
||||
}
|
||||
});
|
||||
|
||||
// Listener for display insets (cutouts) to pass values into native code.
|
||||
View content = getWindow().getDecorView().findViewById(android.R.id.content);
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
|
||||
|
@ -354,9 +391,23 @@ public class MainActivity extends GameActivity {
|
|||
startActivity(Intent.createChooser(intent, "Share image"));
|
||||
}
|
||||
|
||||
// Check if device is using dark theme.
|
||||
// Called from native code to check if device is using dark theme.
|
||||
public boolean useDarkTheme() {
|
||||
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
|
||||
// Called from native code to pick the file.
|
||||
public void pickFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
try {
|
||||
mFilePickResultLauncher.launch(Intent.createChooser(intent, "Pick file"));
|
||||
} catch (android.content.ActivityNotFoundException ex) {
|
||||
onFilePick("");
|
||||
}
|
||||
}
|
||||
|
||||
// Pass picked file into native code.
|
||||
public native void onFilePick(String path);
|
||||
}
|
|
@ -5,6 +5,8 @@ import android.graphics.ImageFormat;
|
|||
import android.graphics.Rect;
|
||||
import android.graphics.YuvImage;
|
||||
import android.media.Image;
|
||||
import android.net.Uri;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -146,4 +148,9 @@ public class Utils {
|
|||
}
|
||||
return directoryToBeDeleted.delete();
|
||||
}
|
||||
|
||||
public static String getFileExtension(Uri uri, Context context) {
|
||||
String fileType = context.getContentResolver().getType(uri);
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,13 +42,13 @@ impl Android {
|
|||
}
|
||||
|
||||
/// Call Android Activity method with JNI.
|
||||
pub fn call_java_method(&self, name: &str, sig: &str, args: &[JValue]) -> Option<jni::sys::jvalue> {
|
||||
pub fn call_java_method(&self, name: &str, s: &str, a: &[JValue]) -> Option<jni::sys::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)
|
||||
};
|
||||
if let Ok(result) = env.call_method(activity, name, sig, args) {
|
||||
if let Ok(result) = env.call_method(activity, name, s, a) {
|
||||
return Some(result.as_jni().clone());
|
||||
}
|
||||
None
|
||||
|
@ -145,6 +145,26 @@ impl PlatformCallbacks for Android {
|
|||
}
|
||||
|
||||
fn pick_file(&self) -> Option<String> {
|
||||
// Clear previous result.
|
||||
let mut w_path = PICKED_FILE_PATH.write();
|
||||
*w_path = None;
|
||||
// Launch file picker.
|
||||
let _ = self.call_java_method("pickFile", "()V", &[]).unwrap();
|
||||
// Return empty string to identify async pick.
|
||||
Some("".to_string())
|
||||
}
|
||||
|
||||
fn picked_file(&self) -> Option<String> {
|
||||
let has_file = {
|
||||
let r_path = PICKED_FILE_PATH.read();
|
||||
r_path.is_some()
|
||||
};
|
||||
if has_file {
|
||||
let mut w_path = PICKED_FILE_PATH.write();
|
||||
let path = Some(w_path.clone().unwrap());
|
||||
*w_path = None;
|
||||
return path
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -152,13 +172,13 @@ impl PlatformCallbacks for Android {
|
|||
lazy_static! {
|
||||
/// Last image data from camera.
|
||||
static ref LAST_CAMERA_IMAGE: Arc<RwLock<Option<(Vec<u8>, u32)>>> = Arc::new(RwLock::new(None));
|
||||
/// Picked file path.
|
||||
static ref PICKED_FILE_PATH: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(target_os = "android")]
|
||||
/// Callback from Java code with last entered character from soft keyboard.
|
||||
#[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,
|
||||
|
@ -170,3 +190,25 @@ pub extern "C" fn Java_mw_gri_android_MainActivity_onCameraImage(
|
|||
let mut w_image = LAST_CAMERA_IMAGE.write();
|
||||
*w_image = Some((image, rotation as u32));
|
||||
}
|
||||
|
||||
/// Callback from Java code with picked file path.
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_mw_gri_android_MainActivity_onFilePick(
|
||||
_env: JNIEnv,
|
||||
_class: JObject,
|
||||
char: jni::sys::jstring
|
||||
) {
|
||||
use std::ops::Add;
|
||||
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_path = PICKED_FILE_PATH.write();
|
||||
*w_path = Some(w_path.clone().unwrap_or("".to_string()).add(str));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -153,6 +153,10 @@ impl PlatformCallbacks for Desktop {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn picked_file(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
|
|
@ -33,4 +33,5 @@ pub trait PlatformCallbacks {
|
|||
fn switch_camera(&self);
|
||||
fn share_data(&self, name: String, data: Vec<u8>) -> Result<(), std::io::Error>;
|
||||
fn pick_file(&self) -> Option<String>;
|
||||
fn picked_file(&self) -> Option<String>;
|
||||
}
|
|
@ -18,12 +18,14 @@ use std::{fs, thread};
|
|||
use parking_lot::RwLock;
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::FILE_ARROW_UP;
|
||||
use crate::gui::icons::ARCHIVE_BOX;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::View;
|
||||
|
||||
/// Button to pick file and parse its data into text.
|
||||
pub struct FilePickButton {
|
||||
/// Flag to check if file is picking.
|
||||
pub file_picking: Arc<AtomicBool>,
|
||||
/// Flag to check if file is parsing.
|
||||
pub file_parsing: Arc<AtomicBool>,
|
||||
/// File parsing result.
|
||||
|
@ -33,6 +35,7 @@ pub struct FilePickButton {
|
|||
impl Default for FilePickButton {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
file_picking: Arc::new(AtomicBool::new(false)),
|
||||
file_parsing: Arc::new(AtomicBool::new(false)),
|
||||
file_parsing_result: Arc::new(RwLock::new(None))
|
||||
}
|
||||
|
@ -45,8 +48,16 @@ impl FilePickButton {
|
|||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
on_result: impl FnOnce(String)) {
|
||||
if self.file_parsing.load(Ordering::Relaxed) {
|
||||
// Draw loading spinner on file parsing.
|
||||
if self.file_picking.load(Ordering::Relaxed) {
|
||||
View::small_loading_spinner(ui);
|
||||
// Check file pick result.
|
||||
if let Some(path) = cb.picked_file() {
|
||||
self.file_picking.store(false, Ordering::Relaxed);
|
||||
if !path.is_empty() {
|
||||
self.on_file_pick(path);
|
||||
}
|
||||
}
|
||||
} else if self.file_parsing.load(Ordering::Relaxed) {
|
||||
View::small_loading_spinner(ui);
|
||||
// Check file parsing result.
|
||||
let has_result = {
|
||||
|
@ -67,10 +78,22 @@ impl FilePickButton {
|
|||
}
|
||||
} else {
|
||||
// Draw button to pick file.
|
||||
let file_text = format!("{} {}", FILE_ARROW_UP, t!("choose_file"));
|
||||
let file_text = format!("{} {}", ARCHIVE_BOX, t!("choose_file"));
|
||||
View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || {
|
||||
if let Some(path) = cb.pick_file() {
|
||||
// Parse file at new thread.
|
||||
self.on_file_pick(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle picked file path.
|
||||
fn on_file_pick(&self, path: String) {
|
||||
// Wait for asynchronous file pick result if path is empty.
|
||||
if path.is_empty() {
|
||||
self.file_picking.store(true, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
self.file_parsing.store(true, Ordering::Relaxed);
|
||||
let result = self.file_parsing_result.clone();
|
||||
thread::spawn(move || {
|
||||
|
@ -81,15 +104,13 @@ impl FilePickButton {
|
|||
//TODO: Detect QR codes on image files.
|
||||
} else {
|
||||
// Parse file as plain text.
|
||||
if let Ok(text) = fs::read_to_string(path) {
|
||||
let mut w_res = result.write();
|
||||
if let Ok(text) = fs::read_to_string(path) {
|
||||
*w_res = Some(text);
|
||||
} else {
|
||||
*w_res = Some("".to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -73,50 +73,6 @@ impl QrCodeContent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw QR code image content.
|
||||
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min += egui::emath::vec2(10.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = egui::epaint::RectShape {
|
||||
rect,
|
||||
rounding: egui::Rounding::default(),
|
||||
fill: egui::Color32::WHITE,
|
||||
stroke: egui::Stroke::NONE,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: egui::Rect::ZERO
|
||||
};
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw QR code image content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.add_space(10.0);
|
||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
||||
let color_img = load_svg_bytes_with_size(svg.as_slice(), Some(size)).unwrap();
|
||||
// Create image texture.
|
||||
let texture_handle = ui.ctx().load_texture("qr_code",
|
||||
color_img.clone(),
|
||||
TextureOptions::default());
|
||||
self.texture_handle = Some(texture_handle.clone());
|
||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||
color_img.height() as f32);
|
||||
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
|
||||
// Add image to content.
|
||||
ui.add(egui::Image::from_texture(sized_img)
|
||||
.max_height(ui.available_width())
|
||||
.fit_to_original_size(1.0));
|
||||
ui.add_space(10.0);
|
||||
}).response.rect;
|
||||
|
||||
// Setup background shape to be painted behind content.
|
||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
|
||||
/// Draw animated QR code content.
|
||||
fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) {
|
||||
if !self.has_image() {
|
||||
|
@ -180,8 +136,9 @@ impl QrCodeContent {
|
|||
});
|
||||
} else {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(8.0);
|
||||
ui.add_space(2.0);
|
||||
View::small_loading_spinner(ui);
|
||||
ui.add_space(1.0);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -271,6 +228,50 @@ impl QrCodeContent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Draw QR code image content.
|
||||
fn qr_image_ui(&mut self, svg: Vec<u8>, ui: &mut egui::Ui) {
|
||||
let mut rect = ui.available_rect_before_wrap();
|
||||
rect.min += egui::emath::vec2(10.0, 0.0);
|
||||
rect.max -= egui::emath::vec2(10.0, 0.0);
|
||||
|
||||
// Create background shape.
|
||||
let mut bg_shape = egui::epaint::RectShape {
|
||||
rect,
|
||||
rounding: egui::Rounding::default(),
|
||||
fill: egui::Color32::WHITE,
|
||||
stroke: egui::Stroke::NONE,
|
||||
fill_texture_id: Default::default(),
|
||||
uv: egui::Rect::ZERO
|
||||
};
|
||||
let bg_idx = ui.painter().add(bg_shape);
|
||||
|
||||
// Draw QR code image content.
|
||||
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.add_space(10.0);
|
||||
let size = SizeHint::Size(ui.available_width() as u32, ui.available_width() as u32);
|
||||
let color_img = load_svg_bytes_with_size(svg.as_slice(), Some(size)).unwrap();
|
||||
// Create image texture.
|
||||
let texture_handle = ui.ctx().load_texture("qr_code",
|
||||
color_img.clone(),
|
||||
TextureOptions::default());
|
||||
self.texture_handle = Some(texture_handle.clone());
|
||||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||||
color_img.height() as f32);
|
||||
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
|
||||
// Add image to content.
|
||||
ui.add(egui::Image::from_texture(sized_img)
|
||||
.max_height(ui.available_width())
|
||||
.fit_to_original_size(1.0));
|
||||
ui.add_space(10.0);
|
||||
}).response.rect;
|
||||
|
||||
// Setup background shape to be painted behind content.
|
||||
content_rect.min -= egui::emath::vec2(10.0, 0.0);
|
||||
content_rect.max += egui::emath::vec2(10.0, 0.0);
|
||||
bg_shape.rect = content_rect;
|
||||
ui.painter().set(bg_idx, bg_shape);
|
||||
}
|
||||
|
||||
/// Check if QR code is loading.
|
||||
fn loading(&self) -> bool {
|
||||
let r_state = self.qr_image_state.read();
|
||||
|
|
Loading…
Reference in a new issue