From 33a5704e6ad018fe5ad8247161dca18ab272e84d Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 14 Apr 2020 22:52:43 +0800 Subject: [PATCH] android/camera: UX enhancements 1. Only request camera permissions once 2. Set the default settings to NDK camera 3. When camera device is not found, fall back to still image 4. Add 'Camera Device' configuration when one is found 5. Added a message when camera permissions are denied For 4, I had to remove the use of the `config` field in StillImage camera. --- .../activities/EmulationActivity.java | 7 ++ .../ui/SettingsFragmentPresenter.java | 81 +++++++++++++++++-- .../app/src/main/jni/camera/ndk_camera.cpp | 55 +++++++++---- .../app/src/main/jni/camera/ndk_camera.h | 8 ++ .../main/jni/camera/still_image_camera.cpp | 3 - src/android/app/src/main/jni/config.cpp | 21 ++--- src/android/app/src/main/jni/default_ini.h | 8 +- .../app/src/main/res/values/arrays.xml | 12 +++ .../app/src/main/res/values/strings.xml | 9 ++- 9 files changed, 169 insertions(+), 35 deletions(-) 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 ca0c0ac1b..00169a8a5 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 @@ -233,6 +233,13 @@ public final class EmulationActivity extends AppCompatActivity { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA: + if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + new AlertDialog.Builder(this) + .setTitle(R.string.camera) + .setMessage(R.string.camera_permission_needed) + .setPositiveButton(android.R.string.ok, null) + .show(); + } NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); break; default: diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java index 3f9993f39..10fdb8c9b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java @@ -1,5 +1,10 @@ package org.citra.citra_emu.features.settings.ui; +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; import android.text.TextUtils; import org.citra.citra_emu.NativeLibrary; @@ -18,8 +23,11 @@ import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSettin import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; import org.citra.citra_emu.features.settings.model.view.PremiumHeader; import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.utils.Log; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; public final class SettingsFragmentPresenter { private SettingsFragmentView mView; @@ -166,29 +174,90 @@ public final class SettingsFragmentPresenter { } private void addCameraSettings(ArrayList sl) { - mView.getActivity().setTitle(R.string.preferences_camera); + final Activity activity = mView.getActivity(); + activity.setTitle(R.string.preferences_camera); - final String[] imageSourceNames = mView.getActivity().getResources().getStringArray(R.array.cameraImageSourceNames); - final String[] imageSourceValues = mView.getActivity().getResources().getStringArray(R.array.cameraImageSourceValues); + // Get the camera IDs + CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + ArrayList supportedCameraNameList = new ArrayList<>(); + ArrayList supportedCameraIdList = new ArrayList<>(); + if (cameraManager != null) { + try { + for (String id : cameraManager.getCameraIdList()) { + final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { + continue; // Legacy cameras cannot be used with the NDK + } + + supportedCameraIdList.add(id); + + final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING)); + switch (facing) { + case CameraCharacteristics.LENS_FACING_FRONT: + supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(R.string.camera_facing_front))); + break; + case CameraCharacteristics.LENS_FACING_BACK: + supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(R.string.camera_facing_back))); + break; + case CameraCharacteristics.LENS_FACING_EXTERNAL: + supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(R.string.camera_facing_external))); + break; + } + } + } catch (CameraAccessException e) { + Log.error("Couldn't retrieve camera list"); + e.printStackTrace(); + } + } + + // Create the names and values for display + ArrayList cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames))); + cameraDeviceNameList.addAll(supportedCameraNameList); + ArrayList cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues))); + cameraDeviceValueList.addAll(supportedCameraIdList); + + final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{}); + final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{}); + + final boolean haveCameraDevices = !supportedCameraIdList.isEmpty(); + + String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames); + String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues); + if (!haveCameraDevices) { + // Remove the last entry (ndk / Device Camera) + imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1); + imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1); + } + + final String defaultImageSource = haveCameraDevices ? "ndk" : "image"; SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA); Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME); + Setting innerCameraConfig = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG); Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP); sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0)); - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, imageSourceValues[0], innerCameraImageSource)); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource)); + if (haveCameraDevices) + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig)); sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip)); Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME); + Setting outerLeftCameraConfig = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG); Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP); sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0)); - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, imageSourceValues[0], outerLeftCameraImageSource)); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource)); + if (haveCameraDevices) + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig)); sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip)); Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME); + Setting outerRightCameraConfig = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG); Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP); sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0)); - sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, imageSourceValues[0], outerRightCameraImageSource)); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource)); + if (haveCameraDevices) + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig)); sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip)); } diff --git a/src/android/app/src/main/jni/camera/ndk_camera.cpp b/src/android/app/src/main/jni/camera/ndk_camera.cpp index 67e0f88ee..3bb6396c9 100644 --- a/src/android/app/src/main/jni/camera/ndk_camera.cpp +++ b/src/android/app/src/main/jni/camera/ndk_camera.cpp @@ -14,6 +14,7 @@ #include "common/thread.h" #include "core/frontend/camera/blank_camera.h" #include "jni/camera/ndk_camera.h" +#include "jni/camera/still_image_camera.h" #include "jni/id_cache.h" namespace Camera::NDK { @@ -256,7 +257,9 @@ Interface::Interface(Factory& factory_, const std::string& id_, const Service::C flip == Service::CAM::Flip::Vertical || flip == Service::CAM::Flip::Reverse; } -Interface::~Interface() = default; +Interface::~Interface() { + factory.camera_permission_requested = false; +} void Interface::StartCapture() { session = factory.CreateCaptureSession(id); @@ -404,17 +407,7 @@ std::shared_ptr Factory::CreateCaptureSession(const std::string& std::unique_ptr Factory::Create(const std::string& config, const Service::CAM::Flip& flip) { - if (!manager) { - JNIEnv* env = IDCache::GetEnvForThread(); - jboolean result = env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), - IDCache::GetRequestCameraPermission()); - if (result != JNI_TRUE) { - LOG_ERROR(Service_CAM, "Camera permissions denied"); - return std::make_unique(); - } - - manager.reset(ACameraManager_create()); - } + manager.reset(ACameraManager_create()); ACameraIdList* id_list = nullptr; auto ret = ACameraManager_getCameraIdList(manager.get(), &id_list); @@ -426,9 +419,28 @@ std::unique_ptr Factory::Create(const std::string& config, SCOPE_EXIT({ ACameraManager_deleteCameraIdList(id_list); }); if (id_list->numCameras <= 0) { - LOG_ERROR(Service_CAM, "No camera devices found"); - return std::make_unique(); + LOG_WARNING(Service_CAM, "No camera devices found, falling back to StillImage"); + // TODO: A better way of doing this? + return std::make_unique()->Create("", flip); } + + // Request camera permission + if (!camera_permission_granted) { + if (camera_permission_requested) { // Permissions already denied + return std::make_unique(); + } + camera_permission_requested = true; + + JNIEnv* env = IDCache::GetEnvForThread(); + jboolean result = env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetRequestCameraPermission()); + if (result != JNI_TRUE) { + LOG_ERROR(Service_CAM, "Camera permissions denied"); + return std::make_unique(); + } + camera_permission_granted = true; + } + if (config.empty()) { LOG_WARNING(Service_CAM, "Camera ID not set, using default camera"); return std::make_unique(*this, id_list->cameraIds[0], flip); @@ -439,6 +451,21 @@ std::unique_ptr Factory::Create(const std::string& config, if (config == id) { return std::make_unique(*this, id, flip); } + + if (config != FrontCameraPlaceholder && config != BackCameraPlaceholder) { + continue; + } + + ACameraMetadata* metadata; + ACameraManager_getCameraCharacteristics(manager.get(), id, &metadata); + SCOPE_EXIT({ ACameraMetadata_free(metadata); }); + + ACameraMetadata_const_entry entry; + ACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &entry); + if ((entry.data.i32[0] == ACAMERA_LENS_FACING_FRONT && config == FrontCameraPlaceholder) || + (entry.data.i32[0] == ACAMERA_LENS_FACING_BACK && config == BackCameraPlaceholder)) { + return std::make_unique(*this, id, flip); + } } LOG_ERROR(Service_CAM, "Camera ID {} not found", config); diff --git a/src/android/app/src/main/jni/camera/ndk_camera.h b/src/android/app/src/main/jni/camera/ndk_camera.h index 36f305e55..f8972fd7b 100644 --- a/src/android/app/src/main/jni/camera/ndk_camera.h +++ b/src/android/app/src/main/jni/camera/ndk_camera.h @@ -49,6 +49,10 @@ private: // bool opened{}; // Whether the camera was successfully opened }; +// Placeholders to mean 'use any front/back camera' +constexpr char FrontCameraPlaceholder[] = "_front"; +constexpr char BackCameraPlaceholder[] = "_back"; + class Factory final : public CameraFactory { public: explicit Factory(); @@ -58,6 +62,10 @@ public: const Service::CAM::Flip& flip) override; private: + // Avoid requesting for permisson more than once on each call + bool camera_permission_requested = false; + bool camera_permission_granted = false; + std::shared_ptr CreateCaptureSession(const std::string& id); // The session is cached, to avoid opening the same camera twice. 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 index 699033641..992b65ba7 100644 --- a/src/android/app/src/main/jni/camera/still_image_camera.cpp +++ b/src/android/app/src/main/jni/camera/still_image_camera.cpp @@ -124,9 +124,6 @@ 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); } diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 58fcbb949..13543061c 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -18,6 +18,7 @@ #include "input_common/main.h" #include "input_common/udp/client.h" #include "jni/button_manager.h" +#include "jni/camera/ndk_camera.h" #include "jni/config.h" #include "jni/default_ini.h" @@ -117,7 +118,7 @@ void Config::ReadValues() { Settings::values.frame_limit = static_cast(sdl2_config->GetInteger("Renderer", "frame_limit", 100)); Settings::values.texture_filter_name = - sdl2_config->GetString("Renderer", "texture_filter_name", "none"); + sdl2_config->GetString("Renderer", "texture_filter_name", "none"); Settings::values.render_3d = static_cast( sdl2_config->GetInteger("Renderer", "render_3d", 0)); @@ -205,21 +206,21 @@ void Config::ReadValues() { // Camera using namespace Service::CAM; Settings::values.camera_name[OuterRightCamera] = - sdl2_config->GetString("Camera", "camera_outer_right_name", "blank"); - Settings::values.camera_config[OuterRightCamera] = - sdl2_config->GetString("Camera", "camera_outer_right_config", ""); + sdl2_config->GetString("Camera", "camera_outer_right_name", "ndk"); + Settings::values.camera_config[OuterRightCamera] = sdl2_config->GetString( + "Camera", "camera_outer_right_config", Camera::NDK::BackCameraPlaceholder); Settings::values.camera_flip[OuterRightCamera] = sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0); Settings::values.camera_name[InnerCamera] = - sdl2_config->GetString("Camera", "camera_inner_name", "blank"); - Settings::values.camera_config[InnerCamera] = - sdl2_config->GetString("Camera", "camera_inner_config", ""); + sdl2_config->GetString("Camera", "camera_inner_name", "ndk"); + Settings::values.camera_config[InnerCamera] = sdl2_config->GetString( + "Camera", "camera_inner_config", Camera::NDK::FrontCameraPlaceholder); Settings::values.camera_flip[InnerCamera] = sdl2_config->GetInteger("Camera", "camera_inner_flip", 0); Settings::values.camera_name[OuterLeftCamera] = - sdl2_config->GetString("Camera", "camera_outer_left_name", "blank"); - Settings::values.camera_config[OuterLeftCamera] = - sdl2_config->GetString("Camera", "camera_outer_left_config", ""); + sdl2_config->GetString("Camera", "camera_outer_left_name", "ndk"); + Settings::values.camera_config[OuterLeftCamera] = sdl2_config->GetString( + "Camera", "camera_outer_left_config", Camera::NDK::BackCameraPlaceholder); Settings::values.camera_flip[OuterLeftCamera] = sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0); diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index e86f39c7d..586b898d1 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -245,7 +245,13 @@ init_time = [Camera] # Which camera engine to use for the right outer camera -# blank (default): a dummy camera that always returns black image +# blank: a dummy camera that always returns black image +# image: loads a still image from the storage. When the camera is started, you will be prompted +# to select an image. +# ndk (Default): uses the device camera. You can specify the camera ID to use in the config field. +# If you don't specify an ID, the default setting will be used. For outer cameras, +# the back-facing camera will be used. For the inner camera, the front-facing +# camera will be used. Please note that 'Legacy' cameras are not supported. camera_outer_right_name = # A config string for the right outer camera. Its meaning is defined by the camera engine diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 58235c89c..393976368 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -104,6 +104,18 @@ ndk + + Default + Any Front Camera + Any Back Camera + + + + + _front + _back + + None Horizontal diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index b5426a96a..fdbe6ea57 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -54,7 +54,12 @@ Outer Left Camera Outer Right Camera Image Source - Sets the image source of the virtual camera. You can use an image file, or a device camera. + Sets the image source of the virtual camera. You can use an image file, or a device camera when supported. + Camera Device + If the \"Image Source\" setting is set to \"Device Camera\", this sets the physical camera to use. + Front + Back + External Image Flip @@ -158,4 +163,6 @@ Select Image + Camera + Citra needs to access your camera to emulate the 3DS\'s cameras.\n\nAlternatively, you can also set \"Image Source\" to \"Still Image\" in Camera Settings.