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:
zhupengfei 2020-04-14 22:52:43 +08:00 committed by bunnei
parent e7d628ab51
commit 33a5704e6a
9 changed files with 169 additions and 35 deletions

View File

@ -233,6 +233,13 @@ public final class EmulationActivity extends AppCompatActivity {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) { switch (requestCode) {
case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA: 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); NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
break; break;
default: default:

View File

@ -1,5 +1,10 @@
package org.citra.citra_emu.features.settings.ui; 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 android.text.TextUtils;
import org.citra.citra_emu.NativeLibrary; 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.SubmenuSetting;
import org.citra.citra_emu.features.settings.model.view.PremiumHeader; import org.citra.citra_emu.features.settings.model.view.PremiumHeader;
import org.citra.citra_emu.features.settings.utils.SettingsFile; import org.citra.citra_emu.features.settings.utils.SettingsFile;
import org.citra.citra_emu.utils.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
public final class SettingsFragmentPresenter { public final class SettingsFragmentPresenter {
private SettingsFragmentView mView; private SettingsFragmentView mView;
@ -166,29 +174,90 @@ public final class SettingsFragmentPresenter {
} }
private void addCameraSettings(ArrayList<SettingsItem> sl) { 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); // Get the camera IDs
final String[] imageSourceValues = mView.getActivity().getResources().getStringArray(R.array.cameraImageSourceValues); 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); SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME); 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); Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0)); 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)); 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 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); 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 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)); 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 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); 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 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)); 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));
} }

View File

@ -14,6 +14,7 @@
#include "common/thread.h" #include "common/thread.h"
#include "core/frontend/camera/blank_camera.h" #include "core/frontend/camera/blank_camera.h"
#include "jni/camera/ndk_camera.h" #include "jni/camera/ndk_camera.h"
#include "jni/camera/still_image_camera.h"
#include "jni/id_cache.h" #include "jni/id_cache.h"
namespace Camera::NDK { 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; flip == Service::CAM::Flip::Vertical || flip == Service::CAM::Flip::Reverse;
} }
Interface::~Interface() = default; Interface::~Interface() {
factory.camera_permission_requested = false;
}
void Interface::StartCapture() { void Interface::StartCapture() {
session = factory.CreateCaptureSession(id); 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, std::unique_ptr<CameraInterface> Factory::Create(const std::string& config,
const Service::CAM::Flip& flip) { const Service::CAM::Flip& flip) {
if (!manager) { manager.reset(ACameraManager_create());
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());
}
ACameraIdList* id_list = nullptr; ACameraIdList* id_list = nullptr;
auto ret = ACameraManager_getCameraIdList(manager.get(), &id_list); 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); }); SCOPE_EXIT({ ACameraManager_deleteCameraIdList(id_list); });
if (id_list->numCameras <= 0) { if (id_list->numCameras <= 0) {
LOG_ERROR(Service_CAM, "No camera devices found"); LOG_WARNING(Service_CAM, "No camera devices found, falling back to StillImage");
return std::make_unique<Camera::BlankCamera>(); // 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()) { if (config.empty()) {
LOG_WARNING(Service_CAM, "Camera ID not set, using default camera"); LOG_WARNING(Service_CAM, "Camera ID not set, using default camera");
return std::make_unique<Interface>(*this, id_list->cameraIds[0], flip); 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) { if (config == id) {
return std::make_unique<Interface>(*this, id, flip); 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); LOG_ERROR(Service_CAM, "Camera ID {} not found", config);

View File

@ -49,6 +49,10 @@ private:
// bool opened{}; // Whether the camera was successfully opened // 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 { class Factory final : public CameraFactory {
public: public:
explicit Factory(); explicit Factory();
@ -58,6 +62,10 @@ public:
const Service::CAM::Flip& flip) override; const Service::CAM::Flip& flip) override;
private: 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); std::shared_ptr<CaptureSession> CreateCaptureSession(const std::string& id);
// The session is cached, to avoid opening the same camera twice. // The session is cached, to avoid opening the same camera twice.

View File

@ -124,9 +124,6 @@ std::unique_ptr<CameraInterface> Factory::Create(const std::string& config,
const Service::CAM::Flip& flip) { const Service::CAM::Flip& flip) {
JNIEnv* env = IDCache::GetEnvForThread(); JNIEnv* env = IDCache::GetEnvForThread();
if (!config.empty()) {
return std::make_unique<Interface>(env->NewStringUTF(config.c_str()), flip);
}
if (last_path != nullptr) { if (last_path != nullptr) {
return std::make_unique<Interface>(last_path, flip); return std::make_unique<Interface>(last_path, flip);
} }

View File

@ -18,6 +18,7 @@
#include "input_common/main.h" #include "input_common/main.h"
#include "input_common/udp/client.h" #include "input_common/udp/client.h"
#include "jni/button_manager.h" #include "jni/button_manager.h"
#include "jni/camera/ndk_camera.h"
#include "jni/config.h" #include "jni/config.h"
#include "jni/default_ini.h" #include "jni/default_ini.h"
@ -117,7 +118,7 @@ void Config::ReadValues() {
Settings::values.frame_limit = Settings::values.frame_limit =
static_cast<u16>(sdl2_config->GetInteger("Renderer", "frame_limit", 100)); static_cast<u16>(sdl2_config->GetInteger("Renderer", "frame_limit", 100));
Settings::values.texture_filter_name = 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>( Settings::values.render_3d = static_cast<Settings::StereoRenderOption>(
sdl2_config->GetInteger("Renderer", "render_3d", 0)); sdl2_config->GetInteger("Renderer", "render_3d", 0));
@ -205,21 +206,21 @@ void Config::ReadValues() {
// Camera // Camera
using namespace Service::CAM; using namespace Service::CAM;
Settings::values.camera_name[OuterRightCamera] = Settings::values.camera_name[OuterRightCamera] =
sdl2_config->GetString("Camera", "camera_outer_right_name", "blank"); sdl2_config->GetString("Camera", "camera_outer_right_name", "ndk");
Settings::values.camera_config[OuterRightCamera] = Settings::values.camera_config[OuterRightCamera] = sdl2_config->GetString(
sdl2_config->GetString("Camera", "camera_outer_right_config", ""); "Camera", "camera_outer_right_config", Camera::NDK::BackCameraPlaceholder);
Settings::values.camera_flip[OuterRightCamera] = Settings::values.camera_flip[OuterRightCamera] =
sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0); sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0);
Settings::values.camera_name[InnerCamera] = Settings::values.camera_name[InnerCamera] =
sdl2_config->GetString("Camera", "camera_inner_name", "blank"); sdl2_config->GetString("Camera", "camera_inner_name", "ndk");
Settings::values.camera_config[InnerCamera] = Settings::values.camera_config[InnerCamera] = sdl2_config->GetString(
sdl2_config->GetString("Camera", "camera_inner_config", ""); "Camera", "camera_inner_config", Camera::NDK::FrontCameraPlaceholder);
Settings::values.camera_flip[InnerCamera] = Settings::values.camera_flip[InnerCamera] =
sdl2_config->GetInteger("Camera", "camera_inner_flip", 0); sdl2_config->GetInteger("Camera", "camera_inner_flip", 0);
Settings::values.camera_name[OuterLeftCamera] = Settings::values.camera_name[OuterLeftCamera] =
sdl2_config->GetString("Camera", "camera_outer_left_name", "blank"); sdl2_config->GetString("Camera", "camera_outer_left_name", "ndk");
Settings::values.camera_config[OuterLeftCamera] = Settings::values.camera_config[OuterLeftCamera] = sdl2_config->GetString(
sdl2_config->GetString("Camera", "camera_outer_left_config", ""); "Camera", "camera_outer_left_config", Camera::NDK::BackCameraPlaceholder);
Settings::values.camera_flip[OuterLeftCamera] = Settings::values.camera_flip[OuterLeftCamera] =
sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0); sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0);

View File

@ -245,7 +245,13 @@ init_time =
[Camera] [Camera]
# Which camera engine to use for the right outer 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 = camera_outer_right_name =
# A config string for the right outer camera. Its meaning is defined by the camera engine # A config string for the right outer camera. Its meaning is defined by the camera engine

View File

@ -104,6 +104,18 @@
<item>ndk</item> <item>ndk</item>
</string-array> </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"> <string-array name="cameraFlipNames">
<item>None</item> <item>None</item>
<item>Horizontal</item> <item>Horizontal</item>

View File

@ -54,7 +54,12 @@
<string name="outer_left_camera">Outer Left Camera</string> <string name="outer_left_camera">Outer Left Camera</string>
<string name="outer_right_camera">Outer Right Camera</string> <string name="outer_right_camera">Outer Right Camera</string>
<string name="image_source">Image Source</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> <string name="image_flip">Image Flip</string>
<!-- Graphics settings strings --> <!-- Graphics settings strings -->
@ -158,4 +163,6 @@
<!-- Camera --> <!-- Camera -->
<string name="camera_select_image">Select Image</string> <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> </resources>