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