From 2c95987fbf0b164d6f0f0ccdf3c408fffcd39428 Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Fri, 3 Apr 2020 15:59:02 +0800 Subject: [PATCH] android: Add a still image camera Similar to what is in the Qt frontend, this camera takes a URI to a picture file. When the config is empty, it will open up the gallery and ask the user to pick a picture. The image is then read and cropped from the Java side by the Picasso library, and sent to the native code with android NDK Bitmap API (jnigraphics). The native code handles the format conversion with libyuv. Image flipping is yet to be implemented. --- .../activities/EmulationActivity.java | 9 ++ .../camera/StillImageCameraHelper.java | 81 +++++++++++ src/android/app/src/main/jni/CMakeLists.txt | 2 + .../main/jni/camera/still_image_camera.cpp | 130 ++++++++++++++++++ .../src/main/jni/camera/still_image_camera.h | 55 ++++++++ src/android/app/src/main/jni/id_cache.cpp | 3 + src/android/app/src/main/jni/native.cpp | 9 +- .../app/src/main/res/values/strings.xml | 3 + 8 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java create mode 100644 src/android/app/src/main/jni/camera/still_image_camera.cpp create mode 100644 src/android/app/src/main/jni/camera/still_image_camera.h diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java index c3a057392..297c78560 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -31,6 +31,7 @@ import org.citra.citra_emu.R; import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; import org.citra.citra_emu.features.settings.ui.SettingsActivity; import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.camera.StillImageCameraHelper; import org.citra.citra_emu.fragments.EmulationFragment; import org.citra.citra_emu.utils.ControllerMappingHelper; import org.citra.citra_emu.utils.EmulationMenuSettings; @@ -219,6 +220,14 @@ public final class EmulationActivity extends AppCompatActivity { .show(); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + super.onActivityResult(requestCode, resultCode, result); + if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) { + StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null); + } + } + private void enableFullscreenImmersive() { // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar. mDecorView.setSystemUiVisibility( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java new file mode 100644 index 000000000..e58bf58bb --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java @@ -0,0 +1,81 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.camera; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.provider.MediaStore; + +import com.squareup.picasso.Picasso; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; +import org.citra.citra_emu.utils.FileBrowserHelper; + +import java.io.IOException; + +import androidx.annotation.Nullable; + +// Used in native code. +public final class StillImageCameraHelper { + public static final int REQUEST_CAMERA_FILE_PICKER = 1; + private static final Object filePickerLock = new Object(); + private static @Nullable String filePickerPath; + + // Opens file picker for camera. + public static @Nullable String OpenFilePicker() { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + + // At this point, we are assuming that we already have permissions as they are + // needed to launch a game + emulationActivity.runOnUiThread(() -> { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); + emulationActivity.startActivityForResult( + Intent.createChooser(intent, + emulationActivity.getString(R.string.camera_select_image)), + REQUEST_CAMERA_FILE_PICKER); + }); + + synchronized (filePickerLock) { + try { + filePickerLock.wait(); + } catch (InterruptedException ignored) { + } + } + + return filePickerPath; + } + + // Called from EmulationActivity. + public static void OnFilePickerResult(Intent result) { + if (result == null) { + filePickerPath = null; + } else { + filePickerPath = result.getDataString(); + } + + synchronized (filePickerLock) { + filePickerLock.notifyAll(); + } + } + + // Blocking call. Load image from file and crop/resize it to fit in width x height. + @Nullable + public static Bitmap LoadImageFromFile(String uri, int width, int height) { + try { + return Picasso.get() + .load(Uri.parse(uri)) + .config(Bitmap.Config.ARGB_8888) + .centerCrop() + .resize(width, height) + .get(); + } catch (IOException e) { + return null; + } + } +} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index a5b72fa1b..3ce02730b 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -3,6 +3,8 @@ add_library(main SHARED applets/swkbd.h button_manager.cpp button_manager.h + camera/still_image_camera.cpp + camera/still_image_camera.h config.cpp config.h default_ini.h diff --git a/src/android/app/src/main/jni/camera/still_image_camera.cpp b/src/android/app/src/main/jni/camera/still_image_camera.cpp new file mode 100644 index 000000000..5ae4a3b0a --- /dev/null +++ b/src/android/app/src/main/jni/camera/still_image_camera.cpp @@ -0,0 +1,130 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "core/frontend/camera/blank_camera.h" +#include "jni/camera/still_image_camera.h" +#include "jni/id_cache.h" + +static jclass s_still_image_camera_helper_class; +static jmethodID s_open_file_picker; +static jmethodID s_load_image_from_file; + +namespace Camera::StillImage { + +void InitJNI(JNIEnv* env) { + s_still_image_camera_helper_class = reinterpret_cast( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/camera/StillImageCameraHelper"))); + s_open_file_picker = env->GetStaticMethodID(s_still_image_camera_helper_class, "OpenFilePicker", + "()Ljava/lang/String;"); + s_load_image_from_file = + env->GetStaticMethodID(s_still_image_camera_helper_class, "LoadImageFromFile", + "(Ljava/lang/String;II)Landroid/graphics/Bitmap;"); +} + +void CleanupJNI(JNIEnv* env) { + env->DeleteGlobalRef(s_still_image_camera_helper_class); +} + +Interface::Interface(jstring path_, const Service::CAM::Flip& flip_) : path(path_), flip(flip_) {} + +Interface::~Interface() { + Factory::last_path = nullptr; +} + +void Interface::StartCapture() { + JNIEnv* env = IDCache::GetEnvForThread(); + jobject bitmap = + env->CallStaticObjectMethod(s_still_image_camera_helper_class, s_load_image_from_file, path, + resolution.width, resolution.height); + if (bitmap == nullptr) { + LOG_ERROR(Frontend, "Could not load image from file"); + opened = false; + return; + } + + int ret; + +#define BITMAP_CALL(func) \ + ret = AndroidBitmap_##func; \ + if (ret != ANDROID_BITMAP_RESULT_SUCCESS) { \ + LOG_ERROR(Frontend, #func " failed with code {}", ret); \ + opened = false; \ + return; \ + } + + AndroidBitmapInfo info; + BITMAP_CALL(getInfo(env, bitmap, &info)); + ASSERT_MSG(info.format == AndroidBitmapFormat::ANDROID_BITMAP_FORMAT_RGBA_8888, + "Bitmap format was incorrect"); + ASSERT_MSG(info.width == resolution.width && info.height == resolution.height, + "Bitmap resolution was incorrect"); + + void* raw_data; + BITMAP_CALL(lockPixels(env, bitmap, &raw_data)); + std::vector data(info.height * info.stride); + libyuv::ABGRToARGB(reinterpret_cast(raw_data), info.stride, data.data(), info.stride, + info.width, info.height); + BITMAP_CALL(unlockPixels(env, bitmap)); + + image.resize(info.height * info.width); + if (format == Service::CAM::OutputFormat::RGB565) { + libyuv::ARGBToRGB565(data.data(), info.stride, reinterpret_cast(image.data()), + info.width * 2, info.width, info.height); + } else { + libyuv::ARGBToYUY2(data.data(), info.stride, reinterpret_cast(image.data()), + info.width * 2, info.width, info.height); + } + opened = true; + +#undef BITMAP_CALL +} + +void Interface::SetResolution(const Service::CAM::Resolution& resolution_) { + resolution = resolution_; +} + +void Interface::SetFlip(Service::CAM::Flip flip_) { + flip = flip_; +} + +void Interface::SetFormat(Service::CAM::OutputFormat format_) { + format = format_; +} + +std::vector Interface::ReceiveFrame() { + return image; +} + +bool Interface::IsPreviewAvailable() { + return opened; +} + +jstring Factory::last_path{}; + +std::unique_ptr Factory::Create(const std::string& config, + const Service::CAM::Flip& flip) { + + JNIEnv* env = IDCache::GetEnvForThread(); + if (!config.empty()) { + return std::make_unique(env->NewStringUTF(config.c_str()), flip); + } + if (last_path != nullptr) { + return std::make_unique(last_path, flip); + } + + // Open file picker to get the string + jstring path = reinterpret_cast( + env->CallStaticObjectMethod(s_still_image_camera_helper_class, s_open_file_picker)); + if (path == nullptr) { + return std::make_unique(); + } else { + last_path = path; + return std::make_unique(path, flip); + } +} + +} // namespace Camera::StillImage diff --git a/src/android/app/src/main/jni/camera/still_image_camera.h b/src/android/app/src/main/jni/camera/still_image_camera.h new file mode 100644 index 000000000..89d40beb9 --- /dev/null +++ b/src/android/app/src/main/jni/camera/still_image_camera.h @@ -0,0 +1,55 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include "common/common_types.h" +#include "core/frontend/camera/factory.h" +#include "core/frontend/camera/interface.h" + +namespace Camera::StillImage { + +class Interface final : public CameraInterface { +public: + Interface(jstring path, const Service::CAM::Flip& flip); + ~Interface(); + void StartCapture() override; + void StopCapture() override{}; + void SetResolution(const Service::CAM::Resolution& resolution) override; + void SetFlip(Service::CAM::Flip flip) override; + void SetEffect(Service::CAM::Effect effect) override{}; + void SetFormat(Service::CAM::OutputFormat format) override; + void SetFrameRate(Service::CAM::FrameRate frame_rate) override{}; + std::vector ReceiveFrame() override; + bool IsPreviewAvailable() override; + +private: + jstring path; + Service::CAM::Resolution resolution; + Service::CAM::Flip flip; + Service::CAM::OutputFormat format; + std::vector image; // Data fetched from the frontend + bool opened{}; // Whether the camera was successfully opened +}; + +class Factory final : public CameraFactory { +public: + std::unique_ptr Create(const std::string& config, + const Service::CAM::Flip& flip) override; + +private: + /// Record the path chosen to avoid multiple prompt problem + static jstring last_path; + + friend class Interface; +}; + +void InitJNI(JNIEnv* env); +void CleanupJNI(JNIEnv* env); + +} // namespace Camera::StillImage diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 068162a63..e87656a8c 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -8,6 +8,7 @@ #include "common/logging/log.h" #include "core/settings.h" #include "jni/applets/swkbd.h" +#include "jni/camera/still_image_camera.h" #include "jni/id_cache.h" #include @@ -113,6 +114,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); SoftwareKeyboard::InitJNI(env); + Camera::StillImage::InitJNI(env); return JNI_VERSION; } @@ -125,6 +127,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { env->DeleteGlobalRef(s_native_library_class); SoftwareKeyboard::CleanupJNI(env); + Camera::StillImage::CleanupJNI(env); } #ifdef __cplusplus diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 67b311039..a80d9d0ec 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -16,11 +16,13 @@ #include "common/string_util.h" #include "core/core.h" #include "core/frontend/applets/default_applets.h" +#include "core/frontend/camera/factory.h" #include "core/frontend/scope_acquire_context.h" #include "core/hle/service/am/am.h" #include "core/settings.h" #include "jni/applets/swkbd.h" #include "jni/button_manager.h" +#include "jni/camera/still_image_camera.h" #include "jni/config.h" #include "jni/emu_window/emu_window.h" #include "jni/game_info.h" @@ -106,6 +108,8 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { Config{}; Settings::Apply(); + Camera::RegisterFactory("image", std::make_unique()); + // Register frontend applets Frontend::RegisterDefaultApplets(); system.RegisterSoftwareKeyboard(std::make_shared()); @@ -162,9 +166,8 @@ void Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, LOG_INFO(Frontend, "Surface changed"); } -void Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, - [[maybe_unused]] [ - [maybe_unused]] jclass clazz) { +void Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed( + JNIEnv* env, [[maybe_unused]][[maybe_unused]] jclass clazz) { ANativeWindow_release(s_surf); s_surf = nullptr; if (window) { diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index dfdd7485e..aab17c2b2 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -146,4 +146,7 @@ Text is too long (should be no more than %d characters) Blank input is not allowed Empty input is not allowed + + + Select Image