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.
This commit is contained in:
parent
87a0b488c1
commit
becbb04785
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
130
src/android/app/src/main/jni/camera/still_image_camera.cpp
Normal file
130
src/android/app/src/main/jni/camera/still_image_camera.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <android/bitmap.h>
|
||||
#include <libyuv.h>
|
||||
#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<jclass>(
|
||||
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<u8> data(info.height * info.stride);
|
||||
libyuv::ABGRToARGB(reinterpret_cast<u8*>(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<u8*>(image.data()),
|
||||
info.width * 2, info.width, info.height);
|
||||
} else {
|
||||
libyuv::ARGBToYUY2(data.data(), info.stride, reinterpret_cast<u8*>(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<u16> Interface::ReceiveFrame() {
|
||||
return image;
|
||||
}
|
||||
|
||||
bool Interface::IsPreviewAvailable() {
|
||||
return opened;
|
||||
}
|
||||
|
||||
jstring Factory::last_path{};
|
||||
|
||||
std::unique_ptr<CameraInterface> Factory::Create(const std::string& config,
|
||||
const Service::CAM::Flip& flip) {
|
||||
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
if (!config.empty()) {
|
||||
return std::make_unique<Interface>(env->NewStringUTF(config.c_str()), flip);
|
||||
}
|
||||
if (last_path != nullptr) {
|
||||
return std::make_unique<Interface>(last_path, flip);
|
||||
}
|
||||
|
||||
// Open file picker to get the string
|
||||
jstring path = reinterpret_cast<jstring>(
|
||||
env->CallStaticObjectMethod(s_still_image_camera_helper_class, s_open_file_picker));
|
||||
if (path == nullptr) {
|
||||
return std::make_unique<Camera::BlankCamera>();
|
||||
} else {
|
||||
last_path = path;
|
||||
return std::make_unique<Interface>(path, flip);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Camera::StillImage
|
55
src/android/app/src/main/jni/camera/still_image_camera.h
Normal file
55
src/android/app/src/main/jni/camera/still_image_camera.h
Normal file
@ -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 <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <jni.h>
|
||||
#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<u16> ReceiveFrame() override;
|
||||
bool IsPreviewAvailable() override;
|
||||
|
||||
private:
|
||||
jstring path;
|
||||
Service::CAM::Resolution resolution;
|
||||
Service::CAM::Flip flip;
|
||||
Service::CAM::OutputFormat format;
|
||||
std::vector<u16> image; // Data fetched from the frontend
|
||||
bool opened{}; // Whether the camera was successfully opened
|
||||
};
|
||||
|
||||
class Factory final : public CameraFactory {
|
||||
public:
|
||||
std::unique_ptr<CameraInterface> 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
|
@ -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 <jni.h>
|
||||
@ -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
|
||||
|
@ -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<Camera::StillImage::Factory>());
|
||||
|
||||
// Register frontend applets
|
||||
Frontend::RegisterDefaultApplets();
|
||||
system.RegisterSoftwareKeyboard(std::make_shared<SoftwareKeyboard::AndroidKeyboard>());
|
||||
@ -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) {
|
||||
|
@ -146,4 +146,7 @@
|
||||
<string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string>
|
||||
<string name="blank_input_not_allowed">Blank input is not allowed</string>
|
||||
<string name="empty_input_not_allowed">Empty input is not allowed</string>
|
||||
|
||||
<!-- Camera -->
|
||||
<string name="camera_select_image">Select Image</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user