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.
This commit is contained in:
parent
70725a6cde
commit
1db947850d
@ -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:
|
||||
|
@ -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<SettingsItem> 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<String> supportedCameraNameList = new ArrayList<>();
|
||||
ArrayList<String> 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<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
|
||||
cameraDeviceNameList.addAll(supportedCameraNameList);
|
||||
ArrayList<String> 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));
|
||||
}
|
||||
|
||||
|
@ -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<CaptureSession> Factory::CreateCaptureSession(const std::string&
|
||||
std::unique_ptr<CameraInterface> 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<Camera::BlankCamera>();
|
||||
}
|
||||
|
||||
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<CameraInterface> 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<Camera::BlankCamera>();
|
||||
LOG_WARNING(Service_CAM, "No camera devices found, falling back to StillImage");
|
||||
// TODO: A better way of doing this?
|
||||
return std::make_unique<StillImage::Factory>()->Create("", flip);
|
||||
}
|
||||
|
||||
// Request camera permission
|
||||
if (!camera_permission_granted) {
|
||||
if (camera_permission_requested) { // Permissions already denied
|
||||
return std::make_unique<Camera::BlankCamera>();
|
||||
}
|
||||
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::BlankCamera>();
|
||||
}
|
||||
camera_permission_granted = true;
|
||||
}
|
||||
|
||||
if (config.empty()) {
|
||||
LOG_WARNING(Service_CAM, "Camera ID not set, using default camera");
|
||||
return std::make_unique<Interface>(*this, id_list->cameraIds[0], flip);
|
||||
@ -439,6 +451,21 @@ std::unique_ptr<CameraInterface> Factory::Create(const std::string& config,
|
||||
if (config == id) {
|
||||
return std::make_unique<Interface>(*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<Interface>(*this, id, flip);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_ERROR(Service_CAM, "Camera ID {} not found", config);
|
||||
|
@ -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<CaptureSession> CreateCaptureSession(const std::string& id);
|
||||
|
||||
// The session is cached, to avoid opening the same camera twice.
|
||||
|
@ -124,9 +124,6 @@ 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);
|
||||
}
|
||||
|
@ -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<u16>(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<Settings::StereoRenderOption>(
|
||||
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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -104,6 +104,18 @@
|
||||
<item>ndk</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="cameraDeviceNames">
|
||||
<item>Default</item>
|
||||
<item>Any Front Camera</item>
|
||||
<item>Any Back Camera</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="cameraDeviceValues">
|
||||
<item/>
|
||||
<item>_front</item>
|
||||
<item>_back</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="cameraFlipNames">
|
||||
<item>None</item>
|
||||
<item>Horizontal</item>
|
||||
|
@ -54,7 +54,12 @@
|
||||
<string name="outer_left_camera">Outer Left Camera</string>
|
||||
<string name="outer_right_camera">Outer Right Camera</string>
|
||||
<string name="image_source">Image Source</string>
|
||||
<string name="image_source_description">Sets the image source of the virtual camera. You can use an image file, or a device camera.</string>
|
||||
<string name="image_source_description">Sets the image source of the virtual camera. You can use an image file, or a device camera when supported.</string>
|
||||
<string name="camera_device">Camera Device</string>
|
||||
<string name="camera_device_description">If the \"Image Source\" setting is set to \"Device Camera\", this sets the physical camera to use.</string>
|
||||
<string name="camera_facing_front">Front</string>
|
||||
<string name="camera_facing_back">Back</string>
|
||||
<string name="camera_facing_external">External</string>
|
||||
<string name="image_flip">Image Flip</string>
|
||||
|
||||
<!-- Graphics settings strings -->
|
||||
@ -158,4 +163,6 @@
|
||||
|
||||
<!-- Camera -->
|
||||
<string name="camera_select_image">Select Image</string>
|
||||
<string name="camera">Camera</string>
|
||||
<string name="camera_permission_needed">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.</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user