mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
strlcpy() begins with strlen() of its source. It lives in another translation unit, so with no LTO that scan happens at runtime even when the source is a literal whose length the compiler already measured, and the call itself cannot be folded away either. strlcpy_lit() passes the length over as sizeof(). When the destination size is also a constant -- sizeof(buf), the usual case -- nothing is left but the stores memcpy() compiles to: strlcpy "core_options" sizeof(buf) 7.01 ns strlcpy_lit "core_options" sizeof(buf) 0.42 ns strlcpy 41-char literal sizeof(buf) 7.74 ns strlcpy_lit 41-char literal sizeof(buf) 0.71 ns strlcpy "core_options" runtime len 7.11 ns strlcpy_lit "core_options" runtime len 0.70 ns The destination bound is kept. When the literal does not fit it defers to strlcpy(), so the copy truncates as before rather than overrunning; a form that took only the literal would have turned every one of these sites into an overflow waiting for a small buffer. The literal is concatenated with "" so a non-literal source is a compile error rather than a sizeof() that silently measures a pointer. 818 sites across 104 files were converted by parsing arguments rather than by pattern-matching text: only a three-argument call whose second argument is a bare string literal is rewritten, so a macro that happens to expand to a literal is left alone. Line endings are preserved, so the CRLF files carry only their own changed lines. test/compat/test_strl.c compares the two against each other over every destination size from 0 past the literal's length, for literals that are empty, single-character, path-shaped, adjacently concatenated and escape-bearing, checking the return, the terminator and the whole buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and TSan both 4/4. The full menu_displaylist_build_list walk over every displaylist type is byte-identical to before the change (md5 a2a081bcac09cdc8d72d67efd14ac40e).
4275 lines
137 KiB
C
4275 lines
137 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
|
|
* Copyright (C) 2011-2017 - Daniel De Matteis
|
|
* Copyright (C) 2012-2015 - Jason Fetters
|
|
* Copyright (C) 2012-2015 - Michael Lelli
|
|
* Copyright (C) 2016-2019 - Andrés Suárez
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
* * You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <sys/utsname.h>
|
|
#include <sys/resource.h>
|
|
|
|
#ifdef __linux__
|
|
#include <linux/version.h>
|
|
#if __STDC_VERSION__ >= 199901L && !defined(ANDROID)
|
|
#include "../../deps/feralgamemode/gamemode_client.h"
|
|
#define FERAL_GAMEMODE
|
|
#endif
|
|
#endif
|
|
|
|
#include <signal.h>
|
|
#include <rthreads/rthreads.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "../../config.h"
|
|
#endif
|
|
|
|
#ifdef ANDROID
|
|
#include <android/log.h>
|
|
#include <sys/system_properties.h>
|
|
#ifdef HAVE_SAF
|
|
#include <vfs/vfs_implementation_saf.h>
|
|
#include "../../menu/menu_cbs.h"
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(DINGUX)
|
|
#include "../../dingux/dingux_utils.h"
|
|
#endif
|
|
|
|
#include <boolean.h>
|
|
#include <libretro.h>
|
|
#include <retro_dirent.h>
|
|
#include <retro_inline.h>
|
|
#include <compat/strl.h>
|
|
#include <compat/fopen_utf8.h>
|
|
#include <lists/file_list.h>
|
|
#include <file/file_path.h>
|
|
#include <streams/file_stream.h>
|
|
#include <string/stdstring.h>
|
|
#include <queues/task_queue.h>
|
|
#include <retro_timers.h>
|
|
#include <features/features_cpu.h>
|
|
|
|
#include "../frontend.h"
|
|
#include "../frontend_driver.h"
|
|
#include "../../defaults.h"
|
|
#include "../../msg_hash.h"
|
|
#include "../../paths.h"
|
|
#include "../../msg_hash_lbl_str.h"
|
|
#include "../../retroarch.h"
|
|
#include "../../configuration.h"
|
|
#include "../../verbosity.h"
|
|
|
|
#ifdef HAVE_MENU
|
|
#include "../../menu/menu_driver.h"
|
|
#include "../../menu/menu_entries.h"
|
|
#else
|
|
#include "../../command.h"
|
|
#endif
|
|
|
|
#include "platform_unix.h"
|
|
|
|
#ifdef WEBOS
|
|
#include <formats/rjson.h>
|
|
#endif
|
|
|
|
#ifdef ANDROID
|
|
static void frontend_unix_set_sustained_performance_mode(bool on);
|
|
|
|
enum
|
|
{
|
|
/* Internal SDCARD writable */
|
|
INTERNAL_STORAGE_WRITABLE = 1,
|
|
/* Internal SDCARD not writable but the private app dir is */
|
|
INTERNAL_STORAGE_APPDIR_WRITABLE,
|
|
/* Internal SDCARD not writable at all */
|
|
INTERNAL_STORAGE_NOT_WRITABLE
|
|
};
|
|
|
|
enum platform_android_flags
|
|
{
|
|
PLAT_ANDROID_FLAG_GAME_CONSOLE_DEVICE = (1 << 0),
|
|
PLAT_ANDROID_FLAG_ANDROID_TV_DEVICE = (1 << 1),
|
|
PLAT_ANDROID_FLAG_XPERIA_PLAY_DEVICE = (1 << 2)
|
|
};
|
|
|
|
static sthread_tls_t thread_key;
|
|
static bool thread_key_inited = false;
|
|
static char app_dir[DIR_MAX_LENGTH];
|
|
static char apk_dir[DIR_MAX_LENGTH];
|
|
static char android_config_path[PATH_MAX_LENGTH];
|
|
/* Set in android_app_create on the UI thread, so the
|
|
* permissionsResolved upcall can reach the app state before the
|
|
* native thread has published g_android. */
|
|
static struct android_app *g_android_early = NULL;
|
|
unsigned storage_permissions = 0;
|
|
struct android_app *g_android = NULL;
|
|
static uint8_t g_platform_android_flags = 0;
|
|
|
|
#ifdef HAVE_SAF
|
|
static struct retro_vfs_authorized_location *android_vfs_authorized_locations = NULL;
|
|
static size_t android_vfs_authorized_locations_count = 0;
|
|
#endif
|
|
#else
|
|
#define PROC_APM_PATH "/proc/apm"
|
|
#define PROC_ACPI_BATTERY_PATH "/proc/acpi/battery"
|
|
#define PROC_ACPI_SYSFS_AC_ADAPTER_PATH "/sys/class/power_supply/ACAD"
|
|
#define PROC_ACPI_SYSFS_BATTERY_PATH "/sys/class/power_supply"
|
|
#define PROC_ACPI_AC_ADAPTER_PATH "/proc/acpi/ac_adapter"
|
|
static char unix_cpu_model_name[64] = {0};
|
|
#endif
|
|
|
|
|
|
#if (defined(__linux__) || defined(__HAIKU__) || defined(__unix__)) && !defined(ANDROID)
|
|
static int speak_pid = 0;
|
|
#endif
|
|
|
|
static volatile sig_atomic_t unix_sighandler_quit;
|
|
|
|
#ifndef ANDROID
|
|
static enum frontend_fork unix_fork_mode = FRONTEND_FORK_NONE;
|
|
#endif
|
|
|
|
int system_property_get(const char *command,
|
|
const char *args, char *value, size_t value_size)
|
|
{
|
|
FILE *pipe;
|
|
char buffer[BUFSIZ];
|
|
char cmd[NAME_MAX_LENGTH];
|
|
char *pos = NULL;
|
|
size_t __len = 0;
|
|
size_t _len = strlcpy(cmd, command, sizeof(cmd));
|
|
|
|
if (value_size == 0)
|
|
return 0;
|
|
|
|
cmd[ _len] = ' ';
|
|
cmd[++_len] = '\0';
|
|
|
|
strlcpy(cmd + _len, args, sizeof(cmd) - _len);
|
|
|
|
if (!(pipe = popen(cmd, "r")))
|
|
{
|
|
RARCH_ERR("Could not create pipe.\n");
|
|
return 0;
|
|
}
|
|
|
|
pos = value;
|
|
|
|
while (!feof(pipe))
|
|
{
|
|
if (fgets(buffer, sizeof(buffer), pipe))
|
|
{
|
|
size_t _len = strlen(buffer);
|
|
|
|
/* Prevent buffer overflow by checking available space */
|
|
if (__len + _len >= value_size - 1)
|
|
{
|
|
/* Copy only what fits, leaving space for null terminator */
|
|
size_t remaining = value_size - __len - 1;
|
|
if (remaining > 0)
|
|
{
|
|
memcpy(pos, buffer, remaining);
|
|
pos += remaining;
|
|
__len += remaining;
|
|
}
|
|
break;
|
|
}
|
|
|
|
memcpy(pos, buffer, _len);
|
|
|
|
pos += _len;
|
|
__len += _len;
|
|
}
|
|
}
|
|
|
|
*pos = '\0';
|
|
pclose(pipe);
|
|
return __len;
|
|
}
|
|
|
|
bool test_permissions(const char *path)
|
|
{
|
|
char buf[PATH_MAX_LENGTH];
|
|
bool ret = false;
|
|
|
|
fill_pathname_join_special(buf, path, ".retroarch", sizeof(buf));
|
|
ret = path_mkdir(buf);
|
|
|
|
if (ret)
|
|
rmdir(buf);
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef ANDROID
|
|
/* forward declaration */
|
|
bool android_run_events(void *data);
|
|
|
|
/* Returns false when the command could not be queued. No
|
|
* acknowledgement for it will ever arrive in that case, so a caller
|
|
* that blocks on one must not take a ticket for it. */
|
|
bool android_app_write_cmd(struct android_app *android_app, int8_t cmd)
|
|
{
|
|
ssize_t ret;
|
|
|
|
if (!android_app)
|
|
return false;
|
|
|
|
/* ART suspends threads with a signal, and the handler is not
|
|
* guaranteed to carry SA_RESTART, so a one-byte pipe write can come
|
|
* back short. It cannot come back partial: PIPE_BUF-sized writes are
|
|
* atomic. */
|
|
do
|
|
{
|
|
ret = write(android_app->msgwrite, &cmd, sizeof(cmd));
|
|
} while (ret < 0 && errno == EINTR);
|
|
|
|
if (ret == (ssize_t)sizeof(cmd))
|
|
return true;
|
|
|
|
RARCH_ERR("[Android] Failed to queue app command %d.\n", (int)cmd);
|
|
return false;
|
|
}
|
|
|
|
static void android_app_set_input(struct android_app *android_app,
|
|
AInputQueue* inputQueue)
|
|
{
|
|
unsigned ticket;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
slock_lock(android_app->mutex);
|
|
android_app->pendingInputQueue = inputQueue;
|
|
ticket = android_app->cmd_seq;
|
|
|
|
if (android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED))
|
|
ticket = ++android_app->cmd_seq;
|
|
|
|
while ((int)(android_app->done_seq - ticket) < 0)
|
|
scond_wait(android_app->cond, android_app->mutex);
|
|
|
|
slock_unlock(android_app->mutex);
|
|
}
|
|
|
|
/* Replacing a live window posts two commands, so wait on the ticket of
|
|
* the last one: the surface is only safe to hand back to the framework
|
|
* once the app thread has worked through both. Posting neither leaves
|
|
* 'ticket' at the current completion count and the wait falls through. */
|
|
static void android_app_set_window(struct android_app *android_app,
|
|
ANativeWindow* window)
|
|
{
|
|
unsigned ticket;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
slock_lock(android_app->mutex);
|
|
ticket = android_app->cmd_seq;
|
|
|
|
if ( android_app->pendingWindow
|
|
&& android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW))
|
|
ticket = ++android_app->cmd_seq;
|
|
|
|
android_app->pendingWindow = window;
|
|
|
|
if ( window
|
|
&& android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW))
|
|
ticket = ++android_app->cmd_seq;
|
|
|
|
while ((int)(android_app->done_seq - ticket) < 0)
|
|
scond_wait(android_app->cond, android_app->mutex);
|
|
|
|
slock_unlock(android_app->mutex);
|
|
}
|
|
|
|
/* Upper bound on how long a lifecycle callback will block waiting for the
|
|
* app thread to pick the command up. The app thread only reads the command
|
|
* pipe between frames, so anything that keeps it out of the looper - a core
|
|
* load, a shader build - holds the UI thread here, and five seconds of that
|
|
* is an ANR. */
|
|
#define ANDROID_ACTIVITY_STATE_TIMEOUT_US (3 * 1000 * 1000)
|
|
|
|
/* START/RESUME/PAUSE/STOP are notifications: activityState is written by
|
|
* the app thread and read by nothing else, so giving up on the
|
|
* acknowledgement costs the caller nothing beyond returning before the app
|
|
* thread has caught up.
|
|
*
|
|
* This does not generalise to android_app_set_window() or
|
|
* android_app_set_input(), where returning early hands the framework an
|
|
* ANativeWindow or AInputQueue the app thread still holds a reference to.
|
|
* Those two must stay synchronous. */
|
|
static void android_app_set_activity_state(
|
|
struct android_app *android_app, int8_t cmd)
|
|
{
|
|
bool acked = true;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
slock_lock(android_app->mutex);
|
|
android_app_write_cmd(android_app, cmd);
|
|
while (android_app->activityState != cmd && acked)
|
|
acked = scond_wait_timeout(android_app->cond, android_app->mutex,
|
|
ANDROID_ACTIVITY_STATE_TIMEOUT_US);
|
|
acked = (android_app->activityState == cmd);
|
|
slock_unlock(android_app->mutex);
|
|
|
|
if (!acked)
|
|
RARCH_ERR("[Android] App thread did not acknowledge activity state"
|
|
" %d.\n", (int)cmd);
|
|
}
|
|
|
|
/* Upper bound on how long onDestroy() will block waiting for the app
|
|
* thread to unwind. ActivityManager gives a destroying activity on the
|
|
* order of ten seconds before it stops waiting, so stay well inside
|
|
* that: overrunning it buys nothing and turns a slow exit into an ANR. */
|
|
#define ANDROID_DESTROY_TIMEOUT_US (5 * 1000 * 1000)
|
|
|
|
static void android_app_free(struct android_app* android_app)
|
|
{
|
|
bool acked;
|
|
|
|
/* Nothing ever wrote APP_CMD_DESTROY, so destroyRequested was dead
|
|
* and the app thread was never told to stop - while this function
|
|
* joined it holding the very mutex the thread needs to finish. If
|
|
* onDestroy() arrived with the thread still running, the Java UI
|
|
* thread blocked here until ActivityManager gave up.
|
|
*
|
|
* Ask the thread to shut down, then wait on the condvar (which
|
|
* releases the mutex, so the thread can take it in
|
|
* android_app_destroy). */
|
|
slock_lock(android_app->mutex);
|
|
|
|
android_app->destroy_from_framework = 1;
|
|
android_app_write_cmd(android_app, APP_CMD_DESTROY);
|
|
|
|
acked = true;
|
|
while (!android_app->destroyed && acked)
|
|
acked = scond_wait_timeout(android_app->cond, android_app->mutex,
|
|
ANDROID_DESTROY_TIMEOUT_US);
|
|
acked = (android_app->destroyed != 0);
|
|
|
|
slock_unlock(android_app->mutex);
|
|
|
|
/* If the thread did not acknowledge it may still be running and still
|
|
* holding references into this struct. Returning without joining lets
|
|
* the framework proceed instead of blocking the UI thread forever;
|
|
* leaking the allocation is the right trade against freeing memory a
|
|
* live thread is using, and the process is being torn down anyway. */
|
|
if (!acked)
|
|
{
|
|
RARCH_ERR("[Android] App thread did not acknowledge destroy; "
|
|
"skipping teardown.\n");
|
|
return;
|
|
}
|
|
|
|
sthread_join(android_app->thread);
|
|
|
|
close(android_app->msgread);
|
|
close(android_app->msgwrite);
|
|
scond_free(android_app->cond);
|
|
slock_free(android_app->mutex);
|
|
|
|
free(android_app);
|
|
}
|
|
|
|
static void onDestroy(ANativeActivity* activity)
|
|
{
|
|
android_app_free((struct android_app*)activity->instance);
|
|
}
|
|
|
|
#ifdef HAVE_ANDROID_LIFECYCLE_HOOKS
|
|
/* Developer hooks: a script named 'switch' run when the activity starts,
|
|
* and one named 'reset' run at teardown, for swapping builds and clearing
|
|
* state on a development device. Absent scripts are not an error.
|
|
*
|
|
* The scripts live in the app's private data directory, which is 0700 and
|
|
* therefore writable only by this app: a hook in shared storage would let
|
|
* anything holding all-files access choose what runs here, with this
|
|
* process's uid and permissions. There is no way to check that after the
|
|
* fact - the FUSE layer over shared storage synthesises ownership, so a
|
|
* stat() of a file another app wrote still reports this app as its owner.
|
|
*
|
|
* The script is handed to the shell to interpret rather than executed:
|
|
* the data directory is exec-blocked for targetSdk 29 and above.
|
|
*
|
|
* Both call sites run on the app thread, so a script holds up the frame
|
|
* loop for as long as it takes rather than a lifecycle callback, and the
|
|
* 'reset' hook spends the teardown budget android_app_free() is waiting
|
|
* out. Build with -DHAVE_ANDROID_LIFECYCLE_HOOKS; release packages do not
|
|
* define it. */
|
|
void android_run_lifecycle_hook(struct android_app *android_app,
|
|
const char *name)
|
|
{
|
|
/* One buffer holds both the path and the command line: the path is
|
|
* written past a fixed 'sh ' prefix, so the existence check reads
|
|
* cmd + 3 and system() reads cmd. Quoting is unnecessary because the
|
|
* path is the internal data directory, whose only variable component
|
|
* is the package name - a Java identifier, so no whitespace and no
|
|
* shell metacharacters. */
|
|
char cmd[DIR_MAX_LENGTH];
|
|
const char *base;
|
|
|
|
if (!android_app || !android_app->activity)
|
|
return;
|
|
|
|
base = android_app->activity->internalDataPath;
|
|
|
|
if (string_is_empty(base))
|
|
return;
|
|
|
|
strlcpy_lit(cmd, "sh ", sizeof(cmd));
|
|
fill_pathname_join_special(cmd + 3, base, name, sizeof(cmd) - 3);
|
|
|
|
if (!path_is_valid(cmd + 3))
|
|
return;
|
|
|
|
RARCH_LOG("[Android] Running lifecycle hook: %s.\n", cmd + 3);
|
|
|
|
if (system(cmd) == -1)
|
|
RARCH_ERR("[Android] Lifecycle hook failed to run: %s.\n", cmd + 3);
|
|
}
|
|
#endif
|
|
|
|
static void onStart(ANativeActivity* activity)
|
|
{
|
|
android_app_set_activity_state((struct android_app*)
|
|
activity->instance, APP_CMD_START);
|
|
}
|
|
|
|
static void onResume(ANativeActivity* activity)
|
|
{
|
|
android_app_set_activity_state((struct android_app*)
|
|
activity->instance, APP_CMD_RESUME);
|
|
}
|
|
|
|
static void* onSaveInstanceState(
|
|
ANativeActivity* activity, size_t* outLen)
|
|
{
|
|
/* RetroArch does not use the Android saved-instance-state blob.
|
|
* android_app->savedState is only ever populated from the blob handed
|
|
* to ANativeActivity_onCreate; nothing produces a new one, and the
|
|
* APP_CMD_SAVE_STATE handler on the app thread did nothing but set
|
|
* the acknowledgement flag.
|
|
*
|
|
* The upstream glue rendezvous here - write the command, wake the app
|
|
* thread, block the UI thread on the condvar until it acknowledges -
|
|
* therefore synchronised a field with no producer, once per
|
|
* backgrounding and once per configuration change (so on every screen
|
|
* rotation). Returning nothing skips the round trip entirely.
|
|
*
|
|
* Handing back the create-time blob, as the previous code did on the
|
|
* first call, would only have re-saved state that was already
|
|
* restored and is never consulted. */
|
|
*outLen = 0;
|
|
return NULL;
|
|
}
|
|
|
|
static void onPause(ANativeActivity* activity)
|
|
{
|
|
android_app_set_activity_state((struct android_app*)
|
|
activity->instance, APP_CMD_PAUSE);
|
|
}
|
|
|
|
static void onStop(ANativeActivity* activity)
|
|
{
|
|
android_app_set_activity_state((struct android_app*)
|
|
activity->instance, APP_CMD_STOP);
|
|
}
|
|
|
|
static void onConfigurationChanged(ANativeActivity *activity)
|
|
{
|
|
android_app_write_cmd((struct android_app*)
|
|
activity->instance, APP_CMD_CONFIG_CHANGED);
|
|
}
|
|
|
|
static void onLowMemory(ANativeActivity* activity)
|
|
{
|
|
android_app_write_cmd((struct android_app*)
|
|
activity->instance, APP_CMD_LOW_MEMORY);
|
|
}
|
|
|
|
static void onWindowFocusChanged(ANativeActivity* activity, int focused)
|
|
{
|
|
android_app_write_cmd((struct android_app*)activity->instance,
|
|
focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
|
|
}
|
|
|
|
static void onNativeWindowCreated(ANativeActivity* activity,
|
|
ANativeWindow* window)
|
|
{
|
|
android_app_set_window((struct android_app*)activity->instance, window);
|
|
}
|
|
|
|
static void onNativeWindowDestroyed(ANativeActivity* activity,
|
|
ANativeWindow* window)
|
|
{
|
|
android_app_set_window((struct android_app*)activity->instance, NULL);
|
|
}
|
|
|
|
static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue)
|
|
{
|
|
android_app_set_input((struct android_app*)activity->instance, queue);
|
|
}
|
|
|
|
static void onInputQueueDestroyed(ANativeActivity* activity,
|
|
AInputQueue* queue)
|
|
{
|
|
android_app_set_input((struct android_app*)activity->instance, NULL);
|
|
}
|
|
|
|
static void onContentRectChanged(ANativeActivity *activity,
|
|
const ARect *rect)
|
|
{
|
|
struct android_app *instance = (struct android_app*)activity->instance;
|
|
int width = rect->right - rect->left;
|
|
int height = rect->bottom - rect->top;
|
|
|
|
/* Store the dimensions before publishing the flag, so a reader that
|
|
* observes @changed cannot still see the previous size and build a
|
|
* swapchain at the wrong resolution. The old code set @changed first
|
|
* and used plain stores, leaving both the ordering and the visibility
|
|
* to chance. */
|
|
retro_atomic_store_release_int(&instance->content_rect.width, width);
|
|
retro_atomic_store_release_int(&instance->content_rect.height, height);
|
|
retro_atomic_store_release_int(&instance->content_rect.changed, 1);
|
|
}
|
|
|
|
JNIEnv *jni_thread_getenv(void)
|
|
{
|
|
JNIEnv *env;
|
|
struct android_app* android_app;
|
|
int status;
|
|
|
|
/* Fast path. A JNIEnv is valid for the entire lifetime of the thread
|
|
* it was handed to, and this is called on only two threads (the app
|
|
* thread and, via the activity callbacks, the Java UI thread), so the
|
|
* cached pointer answers virtually every call.
|
|
*
|
|
* AttachCurrentThread on an already-attached thread is not free: it
|
|
* still crosses into the VM to look the thread up. The TLS slot was
|
|
* already being written below but never read back, so every one of
|
|
* the ~25 call sites - including the per-keystroke KeyCharacterMap
|
|
* lookup - paid for an attach it did not need. */
|
|
if (thread_key_inited)
|
|
if ((env = (JNIEnv*)sthread_tls_get(&thread_key)))
|
|
return env;
|
|
|
|
android_app = (struct android_app*)g_android;
|
|
status = (*android_app->activity->vm)->
|
|
AttachCurrentThread(android_app->activity->vm, &env, 0);
|
|
|
|
if (status < 0)
|
|
{
|
|
RARCH_ERR("jni_thread_getenv: Failed to attach current thread.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Without a key there is nowhere to cache, and jni_thread_destruct
|
|
* will never run to detach - but the env is still usable, so return
|
|
* it rather than failing the call. */
|
|
if (thread_key_inited)
|
|
sthread_tls_set(&thread_key, (void*)env);
|
|
|
|
return env;
|
|
}
|
|
|
|
static void jni_thread_destruct(void *value)
|
|
{
|
|
JNIEnv *env = (JNIEnv*)value;
|
|
struct android_app *android_app = (struct android_app*)g_android;
|
|
|
|
if (!env)
|
|
return;
|
|
|
|
if (android_app)
|
|
(*android_app->activity->vm)->
|
|
DetachCurrentThread(android_app->activity->vm);
|
|
sthread_tls_set(&thread_key, NULL);
|
|
}
|
|
|
|
static void android_app_entry(void *data)
|
|
{
|
|
char arguments[] = "retroarch";
|
|
char *argv[] = {arguments, NULL};
|
|
int argc = 1;
|
|
|
|
sthread_setname("ra-main");
|
|
|
|
rarch_main(argc, argv, data);
|
|
}
|
|
|
|
static struct android_app* android_app_create(ANativeActivity* activity,
|
|
void* savedState, size_t savedStateSize)
|
|
{
|
|
int msgpipe[2];
|
|
struct android_app *android_app =
|
|
(struct android_app*)calloc(1, sizeof(*android_app));
|
|
|
|
if (!android_app)
|
|
{
|
|
RARCH_ERR("Failed to initialize android_app.\n");
|
|
return NULL;
|
|
}
|
|
android_app->activity = activity;
|
|
g_android_early = android_app;
|
|
|
|
android_app->mutex = slock_new();
|
|
android_app->cond = scond_new();
|
|
/* NULL-check slock_new / scond_new: both can fail on OOM.
|
|
* Without the guards here, a NULL mutex would silently turn
|
|
* every slock_lock/unlock below into a no-op (slock_lock
|
|
* NULL-tolerates by design), giving a race-prone android_app,
|
|
* and a NULL cond would NULL-deref in scond_wait below
|
|
* (pthread_cond_wait(&NULL->cond, ...)). Fail the whole
|
|
* android_app construction so ANativeActivity_onCreate returns
|
|
* cleanly without half-initialised state. */
|
|
if (!android_app->mutex || !android_app->cond)
|
|
{
|
|
if (android_app->mutex)
|
|
slock_free(android_app->mutex);
|
|
if (android_app->cond)
|
|
scond_free(android_app->cond);
|
|
free(android_app);
|
|
RARCH_ERR("Failed to allocate android_app locks.\n");
|
|
return NULL;
|
|
}
|
|
|
|
if (savedState)
|
|
{
|
|
android_app->savedState = malloc(savedStateSize);
|
|
/* NULL-check before memcpy on the next line. Android app
|
|
* start with saved state is common (screen rotation,
|
|
* backgrounding/restoration), so this is a realistic OOM
|
|
* path on low-RAM devices. On failure skip the saved-state
|
|
* copy; android_app_entry will start cleanly without it.
|
|
* We can't fail the whole android_app construction here
|
|
* because the app-glue thread already expects to exist. */
|
|
if (android_app->savedState)
|
|
{
|
|
android_app->savedStateSize = savedStateSize;
|
|
memcpy(android_app->savedState, savedState, savedStateSize);
|
|
}
|
|
}
|
|
|
|
if (pipe(msgpipe))
|
|
{
|
|
if (android_app->savedState)
|
|
free(android_app->savedState);
|
|
slock_free(android_app->mutex);
|
|
scond_free(android_app->cond);
|
|
free(android_app);
|
|
return NULL;
|
|
}
|
|
|
|
android_app->msgread = msgpipe[0];
|
|
android_app->msgwrite = msgpipe[1];
|
|
|
|
android_app->thread = sthread_create(android_app_entry, android_app);
|
|
/* NULL-check sthread_create: on OOM the thread won't be
|
|
* spawned and nothing will set android_app->running to true,
|
|
* so the scond_wait loop below would block indefinitely.
|
|
* Tear down the partially-constructed android_app (including
|
|
* the just-created pipe fds) and bail. */
|
|
if (!android_app->thread)
|
|
{
|
|
close(msgpipe[0]);
|
|
close(msgpipe[1]);
|
|
if (android_app->savedState)
|
|
free(android_app->savedState);
|
|
slock_free(android_app->mutex);
|
|
scond_free(android_app->cond);
|
|
free(android_app);
|
|
RARCH_ERR("Failed to spawn android_app thread.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Wait for thread to start. */
|
|
slock_lock(android_app->mutex);
|
|
while (!android_app->running)
|
|
scond_wait(android_app->cond, android_app->mutex);
|
|
slock_unlock(android_app->mutex);
|
|
|
|
return android_app;
|
|
}
|
|
|
|
/*
|
|
* Native activity interaction (called from main thread)
|
|
**/
|
|
|
|
void ANativeActivity_onCreate(ANativeActivity* activity,
|
|
void* savedState, size_t savedStateSize)
|
|
{
|
|
activity->callbacks->onDestroy = onDestroy;
|
|
activity->callbacks->onStart = onStart;
|
|
activity->callbacks->onResume = onResume;
|
|
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
|
|
activity->callbacks->onPause = onPause;
|
|
activity->callbacks->onStop = onStop;
|
|
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
|
|
activity->callbacks->onLowMemory = onLowMemory;
|
|
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
|
|
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
|
|
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
|
|
activity->callbacks->onInputQueueCreated = onInputQueueCreated;
|
|
activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
|
|
activity->callbacks->onContentRectChanged = onContentRectChanged;
|
|
|
|
/* These are set only for the native activity,
|
|
* and are reset when it ends. */
|
|
ANativeActivity_setWindowFlags(activity, AWINDOW_FLAG_KEEP_SCREEN_ON
|
|
| AWINDOW_FLAG_FULLSCREEN, 0);
|
|
|
|
if (sthread_tls_create_with_dtor(&thread_key, jni_thread_destruct))
|
|
thread_key_inited = true;
|
|
else
|
|
RARCH_ERR("Error initializing thread-local storage key.\n");
|
|
|
|
activity->instance = android_app_create(activity,
|
|
savedState, savedStateSize);
|
|
}
|
|
|
|
void frontend_android_get_manufacturer_model(char *s, size_t len)
|
|
{
|
|
char manufacturer[PROP_VALUE_MAX] = {0};
|
|
char model[PROP_VALUE_MAX] = {0};
|
|
|
|
if (!s || len == 0)
|
|
return;
|
|
|
|
__system_property_get("ro.product.manufacturer", manufacturer);
|
|
__system_property_get("ro.product.model", model);
|
|
|
|
if (manufacturer[0])
|
|
manufacturer[0] = (char)toupper((unsigned char)manufacturer[0]);
|
|
|
|
snprintf(s, len, "%s %s", manufacturer, model);
|
|
}
|
|
|
|
void frontend_android_get_name(char *s, size_t len)
|
|
{
|
|
system_property_get("getprop", "ro.product.model", s, len);
|
|
}
|
|
|
|
void frontend_android_get_version(int32_t *major,
|
|
int32_t *minor, int32_t *rel)
|
|
{
|
|
char os_version_str[PROP_VALUE_MAX] = {0};
|
|
__system_property_get("ro.build.version.release",
|
|
os_version_str);
|
|
*major = 0;
|
|
*minor = 0;
|
|
*rel = 0;
|
|
|
|
if (os_version_str[0])
|
|
{
|
|
int32_t *components[] = { major, minor, rel };
|
|
const char *ptr = os_version_str;
|
|
int i;
|
|
|
|
for (i = 0; i < 3 && *ptr; i++)
|
|
{
|
|
char *end;
|
|
long val = strtol(ptr, &end, 10);
|
|
if (end == ptr)
|
|
break;
|
|
*components[i] = (int32_t)val;
|
|
if (*end == '.')
|
|
end++;
|
|
ptr = end;
|
|
}
|
|
}
|
|
}
|
|
|
|
void frontend_android_get_version_sdk(int32_t *sdk)
|
|
{
|
|
char os_version_str[PROP_VALUE_MAX] = {0};
|
|
system_property_get("getprop", "ro.build.version.sdk", os_version_str, sizeof(os_version_str));
|
|
*sdk = 0;
|
|
if (os_version_str[0])
|
|
*sdk = (int32_t)strtol(os_version_str, NULL, 10);
|
|
}
|
|
|
|
static bool device_is_xperia_play(const char *name)
|
|
{
|
|
if (
|
|
strstr(name, "R800x")
|
|
|| strstr(name, "R800at")
|
|
|| strstr(name, "R800i")
|
|
|| strstr(name, "R800a")
|
|
|| strstr(name, "R800")
|
|
|| strstr(name, "Xperia Play")
|
|
|| strstr(name, "SO-01D")
|
|
)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* TODO/FIXME - unfinished */
|
|
static bool device_is_game_console(const char *name)
|
|
{
|
|
if (
|
|
device_is_xperia_play(name)
|
|
|| strstr(name, "OUYA Console")
|
|
|| strstr(name, "GAMEMID_BT")
|
|
|| strstr(name, "S7800")
|
|
|| strstr(name, "XD\n")
|
|
|| strstr(name, "ARCHOS GAMEPAD")
|
|
|| strstr(name, "SHIELD Android TV")
|
|
|| strstr(name, "SHIELD\n")
|
|
)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
* Derivation of launch parameters from the application context.
|
|
*
|
|
* Every device- and installation-derived value the Java launcher passes
|
|
* as an intent extra can also be read here directly, so a launch of the
|
|
* native activity without those extras behaves the same as one through
|
|
* the launcher. Extras always win; these run only for values still
|
|
* unset after the intent has been read.
|
|
* ------------------------------------------------------------------- */
|
|
|
|
/* Clears and reports a pending Java exception; every JNI call below
|
|
* can leave one and must be followed by this check. */
|
|
static bool android_env_exception(JNIEnv *env, const char *what)
|
|
{
|
|
if (!(*env)->ExceptionCheck(env))
|
|
return false;
|
|
(*env)->ExceptionClear(env);
|
|
__android_log_print(ANDROID_LOG_WARN,
|
|
"RetroArch", "[ENV] Derivation failed: %s.\n", what);
|
|
return true;
|
|
}
|
|
|
|
/* Copies a jstring into buf and releases the local reference. */
|
|
static bool android_env_jstring_copy(JNIEnv *env, jstring jstr,
|
|
char *buf, size_t len)
|
|
{
|
|
const char *utf;
|
|
|
|
if (!jstr)
|
|
return false;
|
|
utf = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
if (utf)
|
|
{
|
|
if (*utf)
|
|
strlcpy(buf, utf, len);
|
|
(*env)->ReleaseStringUTFChars(env, jstr, utf);
|
|
}
|
|
(*env)->DeleteLocalRef(env, jstr);
|
|
return (*buf != '\0');
|
|
}
|
|
|
|
/* File.getAbsolutePath() into buf; releases the File reference. */
|
|
static bool android_env_file_path(JNIEnv *env, jobject file,
|
|
char *buf, size_t len)
|
|
{
|
|
jclass file_class;
|
|
jmethodID get_absolute_path;
|
|
jstring jpath;
|
|
|
|
if (!file)
|
|
return false;
|
|
file_class = (*env)->GetObjectClass(env, file);
|
|
get_absolute_path = (*env)->GetMethodID(env, file_class,
|
|
"getAbsolutePath", "()Ljava/lang/String;");
|
|
(*env)->DeleteLocalRef(env, file_class);
|
|
if (android_env_exception(env, "File.getAbsolutePath lookup"))
|
|
{
|
|
(*env)->DeleteLocalRef(env, file);
|
|
return false;
|
|
}
|
|
jpath = (jstring)(*env)->CallObjectMethod(env, file, get_absolute_path);
|
|
(*env)->DeleteLocalRef(env, file);
|
|
if (android_env_exception(env, "File.getAbsolutePath"))
|
|
return false;
|
|
return android_env_jstring_copy(env, jpath, buf, len);
|
|
}
|
|
|
|
/* getApplicationInfo().dataDir / .sourceDir for absent DATADIR / APK. */
|
|
static void android_env_derive_application_info(JNIEnv *env, jobject activity)
|
|
{
|
|
jclass activity_class;
|
|
jclass appinfo_class;
|
|
jmethodID get_application_info;
|
|
jobject appinfo;
|
|
|
|
activity_class = (*env)->GetObjectClass(env, activity);
|
|
get_application_info = (*env)->GetMethodID(env, activity_class,
|
|
"getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
if (android_env_exception(env, "getApplicationInfo lookup"))
|
|
return;
|
|
appinfo = (*env)->CallObjectMethod(env, activity, get_application_info);
|
|
if (android_env_exception(env, "getApplicationInfo") || !appinfo)
|
|
return;
|
|
appinfo_class = (*env)->GetObjectClass(env, appinfo);
|
|
|
|
if (!*app_dir)
|
|
{
|
|
jfieldID fid = (*env)->GetFieldID(env, appinfo_class,
|
|
"dataDir", "Ljava/lang/String;");
|
|
if (!android_env_exception(env, "ApplicationInfo.dataDir"))
|
|
android_env_jstring_copy(env,
|
|
(jstring)(*env)->GetObjectField(env, appinfo, fid),
|
|
app_dir, sizeof(app_dir));
|
|
}
|
|
if (!*apk_dir)
|
|
{
|
|
jfieldID fid = (*env)->GetFieldID(env, appinfo_class,
|
|
"sourceDir", "Ljava/lang/String;");
|
|
if (!android_env_exception(env, "ApplicationInfo.sourceDir"))
|
|
android_env_jstring_copy(env,
|
|
(jstring)(*env)->GetObjectField(env, appinfo, fid),
|
|
apk_dir, sizeof(apk_dir));
|
|
}
|
|
(*env)->DeleteLocalRef(env, appinfo_class);
|
|
(*env)->DeleteLocalRef(env, appinfo);
|
|
}
|
|
|
|
/* getPackageManager().getPackageInfo(getPackageName(), 0).versionCode. */
|
|
static unsigned android_env_derive_version_code(JNIEnv *env, jobject activity)
|
|
{
|
|
jclass activity_class;
|
|
jclass pm_class;
|
|
jclass pi_class;
|
|
jmethodID get_package_manager;
|
|
jmethodID get_package_name;
|
|
jmethodID get_package_info;
|
|
jfieldID version_code_field;
|
|
jobject pm;
|
|
jstring pkg;
|
|
jobject pi;
|
|
jint version = 0;
|
|
|
|
activity_class = (*env)->GetObjectClass(env, activity);
|
|
get_package_manager = (*env)->GetMethodID(env, activity_class,
|
|
"getPackageManager", "()Landroid/content/pm/PackageManager;");
|
|
if (android_env_exception(env, "getPackageManager lookup"))
|
|
{
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
return 0;
|
|
}
|
|
get_package_name = (*env)->GetMethodID(env, activity_class,
|
|
"getPackageName", "()Ljava/lang/String;");
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
if (android_env_exception(env, "getPackageName lookup"))
|
|
return 0;
|
|
|
|
pm = (*env)->CallObjectMethod(env, activity, get_package_manager);
|
|
if (android_env_exception(env, "getPackageManager") || !pm)
|
|
return 0;
|
|
pkg = (jstring)(*env)->CallObjectMethod(env, activity, get_package_name);
|
|
if (android_env_exception(env, "getPackageName") || !pkg)
|
|
{
|
|
(*env)->DeleteLocalRef(env, pm);
|
|
return 0;
|
|
}
|
|
|
|
pm_class = (*env)->GetObjectClass(env, pm);
|
|
get_package_info = (*env)->GetMethodID(env, pm_class,
|
|
"getPackageInfo",
|
|
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
|
|
(*env)->DeleteLocalRef(env, pm_class);
|
|
if (!android_env_exception(env, "getPackageInfo lookup"))
|
|
{
|
|
pi = (*env)->CallObjectMethod(env, pm, get_package_info, pkg, (jint)0);
|
|
if (!android_env_exception(env, "getPackageInfo") && pi)
|
|
{
|
|
pi_class = (*env)->GetObjectClass(env, pi);
|
|
version_code_field = (*env)->GetFieldID(env, pi_class,
|
|
"versionCode", "I");
|
|
if (!android_env_exception(env, "PackageInfo.versionCode"))
|
|
version = (*env)->GetIntField(env, pi, version_code_field);
|
|
(*env)->DeleteLocalRef(env, pi_class);
|
|
(*env)->DeleteLocalRef(env, pi);
|
|
}
|
|
}
|
|
(*env)->DeleteLocalRef(env, pm);
|
|
(*env)->DeleteLocalRef(env, pkg);
|
|
return (unsigned)version;
|
|
}
|
|
|
|
/* Settings.Secure.getString(getContentResolver(), "default_input_method")
|
|
* for an absent IME extra. */
|
|
static void android_env_derive_ime(JNIEnv *env, jobject activity,
|
|
char *buf, size_t len)
|
|
{
|
|
jclass activity_class;
|
|
jclass secure_class;
|
|
jmethodID get_content_resolver;
|
|
jmethodID get_string;
|
|
jobject resolver;
|
|
jstring jval;
|
|
|
|
activity_class = (*env)->GetObjectClass(env, activity);
|
|
get_content_resolver = (*env)->GetMethodID(env, activity_class,
|
|
"getContentResolver", "()Landroid/content/ContentResolver;");
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
if (android_env_exception(env, "getContentResolver lookup"))
|
|
return;
|
|
resolver = (*env)->CallObjectMethod(env, activity, get_content_resolver);
|
|
if (android_env_exception(env, "getContentResolver") || !resolver)
|
|
return;
|
|
|
|
secure_class = (*env)->FindClass(env, "android/provider/Settings$Secure");
|
|
if (!android_env_exception(env, "Settings.Secure") && secure_class)
|
|
{
|
|
get_string = (*env)->GetStaticMethodID(env, secure_class, "getString",
|
|
"(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;");
|
|
if (!android_env_exception(env, "Secure.getString lookup"))
|
|
{
|
|
jval = (jstring)(*env)->CallStaticObjectMethod(env, secure_class,
|
|
get_string, resolver,
|
|
(*env)->NewStringUTF(env, "default_input_method"));
|
|
if (!android_env_exception(env, "Secure.getString"))
|
|
android_env_jstring_copy(env, jval, buf, len);
|
|
}
|
|
(*env)->DeleteLocalRef(env, secure_class);
|
|
}
|
|
(*env)->DeleteLocalRef(env, resolver);
|
|
}
|
|
|
|
/* AudioManager.getProperty() for absent AUDIO_RATE / AUDIO_FRAMES. */
|
|
static void android_env_derive_audio(JNIEnv *env, jobject activity)
|
|
{
|
|
int32_t sdk = 0;
|
|
jclass activity_class;
|
|
jclass am_class;
|
|
jmethodID get_system_service;
|
|
jmethodID get_property;
|
|
jobject am;
|
|
jstring jval;
|
|
char value[32];
|
|
|
|
frontend_android_get_version_sdk(&sdk);
|
|
if (sdk < 17)
|
|
return;
|
|
|
|
activity_class = (*env)->GetObjectClass(env, activity);
|
|
get_system_service = (*env)->GetMethodID(env, activity_class,
|
|
"getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
if (android_env_exception(env, "getSystemService lookup"))
|
|
return;
|
|
am = (*env)->CallObjectMethod(env, activity, get_system_service,
|
|
(*env)->NewStringUTF(env, "audio"));
|
|
if (android_env_exception(env, "getSystemService(audio)") || !am)
|
|
return;
|
|
|
|
am_class = (*env)->GetObjectClass(env, am);
|
|
get_property = (*env)->GetMethodID(env, am_class,
|
|
"getProperty", "(Ljava/lang/String;)Ljava/lang/String;");
|
|
(*env)->DeleteLocalRef(env, am_class);
|
|
if (!android_env_exception(env, "AudioManager.getProperty lookup"))
|
|
{
|
|
if (g_defaults.settings_out_sample_rate <= 0)
|
|
{
|
|
*value = '\0';
|
|
jval = (jstring)(*env)->CallObjectMethod(env, am, get_property,
|
|
(*env)->NewStringUTF(env,
|
|
"android.media.property.OUTPUT_SAMPLE_RATE"));
|
|
if ( !android_env_exception(env, "OUTPUT_SAMPLE_RATE")
|
|
&& android_env_jstring_copy(env, jval, value, sizeof(value)))
|
|
g_defaults.settings_out_sample_rate = atoi(value);
|
|
}
|
|
if (g_defaults.settings_out_block_frames <= 0)
|
|
{
|
|
*value = '\0';
|
|
jval = (jstring)(*env)->CallObjectMethod(env, am, get_property,
|
|
(*env)->NewStringUTF(env,
|
|
"android.media.property.OUTPUT_FRAMES_PER_BUFFER"));
|
|
if ( !android_env_exception(env, "OUTPUT_FRAMES_PER_BUFFER")
|
|
&& android_env_jstring_copy(env, jval, value, sizeof(value)))
|
|
g_defaults.settings_out_block_frames = atoi(value);
|
|
}
|
|
}
|
|
(*env)->DeleteLocalRef(env, am);
|
|
}
|
|
|
|
/* getExternalFilesDir(null) for an absent EXTERNAL extra: the
|
|
* app-external files directory the Java launcher used to pass,
|
|
* /storage/emulated/0/Android/data/<package>/files. The framework
|
|
* creates the directory on this call if it does not exist yet. */
|
|
static void android_env_derive_app_storage(JNIEnv *env, jobject activity)
|
|
{
|
|
jclass activity_class;
|
|
jmethodID get_external_files_dir;
|
|
jobject dir;
|
|
|
|
activity_class = (*env)->GetObjectClass(env, activity);
|
|
get_external_files_dir = (*env)->GetMethodID(env, activity_class,
|
|
"getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;");
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
if (android_env_exception(env, "getExternalFilesDir lookup"))
|
|
return;
|
|
dir = (*env)->CallObjectMethod(env, activity,
|
|
get_external_files_dir, (jobject)NULL);
|
|
if (android_env_exception(env, "getExternalFilesDir") || !dir)
|
|
return;
|
|
/* android_env_file_path releases dir. */
|
|
android_env_file_path(env, dir,
|
|
internal_storage_app_path, sizeof(internal_storage_app_path));
|
|
}
|
|
|
|
/* External storage locations for absent SDCARD / EXTERNAL extras,
|
|
* following the Java launcher: scoped-storage devices without all-files
|
|
* access use the app's external media dir (created on demand) for the
|
|
* shared location; everything else uses the shared storage root. The
|
|
* app-external location comes from getExternalFilesDir, with the
|
|
* shared location as a last resort. */
|
|
static void android_env_derive_storage(JNIEnv *env, jobject activity)
|
|
{
|
|
int32_t sdk = 0;
|
|
bool all_files_access = true;
|
|
jclass env_class;
|
|
jmethodID get_ext_storage_dir;
|
|
jobject dir;
|
|
|
|
frontend_android_get_version_sdk(&sdk);
|
|
|
|
env_class = (*env)->FindClass(env, "android/os/Environment");
|
|
if (android_env_exception(env, "Environment") || !env_class)
|
|
return;
|
|
|
|
if (sdk >= 30)
|
|
{
|
|
jmethodID is_manager = (*env)->GetStaticMethodID(env, env_class,
|
|
"isExternalStorageManager", "()Z");
|
|
if (!android_env_exception(env, "isExternalStorageManager lookup"))
|
|
{
|
|
all_files_access = (*env)->CallStaticBooleanMethod(env,
|
|
env_class, is_manager);
|
|
if (android_env_exception(env, "isExternalStorageManager"))
|
|
all_files_access = false;
|
|
}
|
|
}
|
|
|
|
if (sdk >= 30 && !all_files_access && !*internal_storage_path)
|
|
{
|
|
/* Scoped storage: app external media dir, created on demand. */
|
|
jclass activity_class = (*env)->GetObjectClass(env, activity);
|
|
jmethodID get_media_dirs = (*env)->GetMethodID(env, activity_class,
|
|
"getExternalMediaDirs", "()[Ljava/io/File;");
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
if (!android_env_exception(env, "getExternalMediaDirs lookup"))
|
|
{
|
|
jobjectArray dirs = (jobjectArray)(*env)->CallObjectMethod(env,
|
|
activity, get_media_dirs);
|
|
if ( !android_env_exception(env, "getExternalMediaDirs")
|
|
&& dirs
|
|
&& (*env)->GetArrayLength(env, dirs) > 0)
|
|
{
|
|
dir = (*env)->GetObjectArrayElement(env, dirs, 0);
|
|
if (dir)
|
|
{
|
|
jclass file_class = (*env)->GetObjectClass(env, dir);
|
|
jmethodID mkdirs = (*env)->GetMethodID(env, file_class,
|
|
"mkdirs", "()Z");
|
|
(*env)->DeleteLocalRef(env, file_class);
|
|
if (!android_env_exception(env, "File.mkdirs lookup"))
|
|
{
|
|
(*env)->CallBooleanMethod(env, dir, mkdirs);
|
|
android_env_exception(env, "File.mkdirs");
|
|
}
|
|
/* android_env_file_path releases dir. */
|
|
android_env_file_path(env, dir,
|
|
internal_storage_path, sizeof(internal_storage_path));
|
|
}
|
|
}
|
|
if (dirs)
|
|
(*env)->DeleteLocalRef(env, dirs);
|
|
}
|
|
}
|
|
|
|
if (!*internal_storage_path)
|
|
{
|
|
get_ext_storage_dir = (*env)->GetStaticMethodID(env, env_class,
|
|
"getExternalStorageDirectory", "()Ljava/io/File;");
|
|
if (!android_env_exception(env, "getExternalStorageDirectory lookup"))
|
|
{
|
|
dir = (*env)->CallStaticObjectMethod(env, env_class,
|
|
get_ext_storage_dir);
|
|
if (!android_env_exception(env, "getExternalStorageDirectory"))
|
|
android_env_file_path(env, dir,
|
|
internal_storage_path, sizeof(internal_storage_path));
|
|
}
|
|
}
|
|
(*env)->DeleteLocalRef(env, env_class);
|
|
|
|
if (!*internal_storage_app_path)
|
|
android_env_derive_app_storage(env, activity);
|
|
if (*internal_storage_path && !*internal_storage_app_path)
|
|
strlcpy(internal_storage_app_path, internal_storage_path,
|
|
sizeof(internal_storage_app_path));
|
|
}
|
|
|
|
/* Main config location for an absent CONFIGFILE extra, following the
|
|
* Java launcher's global-config probe order: an existing retroarch.cfg
|
|
* in the app-external files dir, then in the internal files dir; if
|
|
* neither exists yet, the preferred writable location is used and the
|
|
* file is created on the first save. */
|
|
static void android_env_derive_config_path(JNIEnv *env, jobject activity)
|
|
{
|
|
/* Static scratch: this runs once, from the environment pass on the
|
|
* main thread, and these would otherwise dominate the (inlined)
|
|
* caller's stack frame. */
|
|
static char external[DIR_MAX_LENGTH];
|
|
static char internal[DIR_MAX_LENGTH];
|
|
static char candidate[PATH_MAX_LENGTH];
|
|
jclass activity_class;
|
|
jmethodID get_ext_files_dir;
|
|
jmethodID get_files_dir;
|
|
jobject dir;
|
|
|
|
*external = '\0';
|
|
*internal = '\0';
|
|
activity_class = (*env)->GetObjectClass(env, activity);
|
|
|
|
get_ext_files_dir = (*env)->GetMethodID(env, activity_class,
|
|
"getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;");
|
|
if (!android_env_exception(env, "getExternalFilesDir lookup"))
|
|
{
|
|
dir = (*env)->CallObjectMethod(env, activity, get_ext_files_dir,
|
|
(jobject)NULL);
|
|
if (!android_env_exception(env, "getExternalFilesDir"))
|
|
android_env_file_path(env, dir, external, sizeof(external));
|
|
}
|
|
|
|
get_files_dir = (*env)->GetMethodID(env, activity_class,
|
|
"getFilesDir", "()Ljava/io/File;");
|
|
if (!android_env_exception(env, "getFilesDir lookup"))
|
|
{
|
|
dir = (*env)->CallObjectMethod(env, activity, get_files_dir);
|
|
if (!android_env_exception(env, "getFilesDir"))
|
|
android_env_file_path(env, dir, internal, sizeof(internal));
|
|
}
|
|
(*env)->DeleteLocalRef(env, activity_class);
|
|
|
|
if (*external)
|
|
{
|
|
fill_pathname_join(candidate, external, "retroarch.cfg",
|
|
sizeof(candidate));
|
|
if (path_is_valid(candidate))
|
|
{
|
|
strlcpy(android_config_path, candidate, sizeof(android_config_path));
|
|
return;
|
|
}
|
|
}
|
|
if (*internal)
|
|
{
|
|
fill_pathname_join(candidate, internal, "retroarch.cfg",
|
|
sizeof(candidate));
|
|
if (path_is_valid(candidate))
|
|
{
|
|
strlcpy(android_config_path, candidate, sizeof(android_config_path));
|
|
return;
|
|
}
|
|
}
|
|
if (*external)
|
|
fill_pathname_join(android_config_path, external, "retroarch.cfg",
|
|
sizeof(android_config_path));
|
|
else if (*internal)
|
|
fill_pathname_join(android_config_path, internal, "retroarch.cfg",
|
|
sizeof(android_config_path));
|
|
}
|
|
|
|
bool test_permissions_android(const char *path)
|
|
{
|
|
char buf[PATH_MAX_LENGTH];
|
|
bool ret = false;
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "Testing permissions for %s\n",path);
|
|
|
|
fill_pathname_join_special(buf, path, ".retroarch", sizeof(buf));
|
|
ret = path_mkdir(buf);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "Create %s in %s %s\n", buf, path,
|
|
ret ? "true" : "false");
|
|
|
|
if (ret)
|
|
rmdir(buf);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void frontend_android_shutdown(bool unused)
|
|
{
|
|
(void)unused;
|
|
/* Cleaner approaches don't work sadly. */
|
|
exit(0);
|
|
}
|
|
|
|
#ifdef HAVE_SAF
|
|
static void android_vfs_authorized_locations_free(void)
|
|
{
|
|
size_t i;
|
|
|
|
if (!android_vfs_authorized_locations)
|
|
return;
|
|
|
|
for (i = 0; i < android_vfs_authorized_locations_count; i++)
|
|
{
|
|
free((void*)android_vfs_authorized_locations[i].path);
|
|
free((void*)android_vfs_authorized_locations[i].label);
|
|
}
|
|
|
|
free(android_vfs_authorized_locations);
|
|
android_vfs_authorized_locations = NULL;
|
|
android_vfs_authorized_locations_count = 0;
|
|
}
|
|
|
|
static bool android_vfs_authorized_locations_refresh(void)
|
|
{
|
|
JNIEnv *env;
|
|
jarray trees;
|
|
jsize trees_length;
|
|
jsize i;
|
|
|
|
android_vfs_authorized_locations_free();
|
|
|
|
if (!g_android || !g_android->have_saf)
|
|
return false;
|
|
|
|
env = jni_thread_getenv();
|
|
if (!env)
|
|
return false;
|
|
|
|
trees = (*env)->CallObjectMethod(
|
|
env,
|
|
g_android->activity->clazz,
|
|
g_android->getPersistedSafTrees);
|
|
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
return false;
|
|
}
|
|
|
|
if (!trees)
|
|
return false;
|
|
|
|
trees_length = (*env)->GetArrayLength(env, trees);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
(*env)->DeleteLocalRef(env, trees);
|
|
return false;
|
|
}
|
|
|
|
if (trees_length <= 0)
|
|
{
|
|
(*env)->DeleteLocalRef(env, trees);
|
|
return true;
|
|
}
|
|
|
|
android_vfs_authorized_locations =
|
|
(struct retro_vfs_authorized_location*)calloc(
|
|
(size_t)trees_length,
|
|
sizeof(*android_vfs_authorized_locations));
|
|
|
|
if (!android_vfs_authorized_locations)
|
|
{
|
|
(*env)->DeleteLocalRef(env, trees);
|
|
return false;
|
|
}
|
|
|
|
for (i = 0; i < trees_length; ++i)
|
|
{
|
|
jstring tree;
|
|
const char *tree_chars;
|
|
char *serialized_path;
|
|
|
|
tree = (jstring)(*env)->GetObjectArrayElement(env, trees, i);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
continue;
|
|
}
|
|
|
|
tree_chars = (*env)->GetStringUTFChars(env, tree, NULL);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
(*env)->DeleteLocalRef(env, tree);
|
|
continue;
|
|
}
|
|
|
|
serialized_path = retro_vfs_path_join_saf(tree_chars, "");
|
|
|
|
if (serialized_path)
|
|
{
|
|
const char *label = msg_hash_to_str(MSG_REMOVABLE_STORAGE);
|
|
|
|
android_vfs_authorized_locations[android_vfs_authorized_locations_count].path = serialized_path;
|
|
android_vfs_authorized_locations[android_vfs_authorized_locations_count].label = strdup(label ? label : "Storage");
|
|
android_vfs_authorized_locations[android_vfs_authorized_locations_count].flags = 0;
|
|
|
|
android_vfs_authorized_locations_count++;
|
|
}
|
|
|
|
(*env)->ReleaseStringUTFChars(env, tree, tree_chars);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
|
|
(*env)->DeleteLocalRef(env, tree);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
}
|
|
|
|
(*env)->DeleteLocalRef(env, trees);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool android_get_vfs_authorized_locations(
|
|
struct retro_vfs_authorized_locations *locations)
|
|
{
|
|
if (!g_android || !g_android->have_saf)
|
|
return false;
|
|
|
|
if (!android_vfs_authorized_locations && !android_vfs_authorized_locations_refresh())
|
|
return false;
|
|
|
|
if (!locations)
|
|
return true;
|
|
|
|
locations->locations = android_vfs_authorized_locations;
|
|
locations->count = android_vfs_authorized_locations_count;
|
|
|
|
return true;
|
|
}
|
|
|
|
void android_show_saf_tree_picker(void)
|
|
{
|
|
JNIEnv *env;
|
|
|
|
if (!g_android || !g_android->have_saf)
|
|
return;
|
|
|
|
env = jni_thread_getenv();
|
|
if (!env)
|
|
return;
|
|
|
|
CALL_VOID_METHOD(env, g_android->activity->clazz, g_android->requestOpenDocumentTree);
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Class: com_retroarch_browser_retroactivity_RetroActivityCommon
|
|
* Method: safTreeAdded
|
|
* Signature: (Ljava/lang/String;)V
|
|
*/
|
|
/* Signals from the Java UI thread that the startup storage-permission
|
|
* flow has finished, releasing the gate in frontend_unix_init. The
|
|
* wake is sticky, so a signal that lands before the native thread has
|
|
* reached the gate is not lost; a signal before the looper exists is
|
|
* covered by the gate re-checking the state before every poll. */
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCommon_permissionsResolved
|
|
(JNIEnv *env, jobject this_obj, jboolean granted)
|
|
{
|
|
struct android_app *android_app = g_android_early;
|
|
ALooper *looper = NULL;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
slock_lock(android_app->mutex);
|
|
android_app->permission_state |= PLAT_ANDROID_PERM_RESOLVED;
|
|
if (granted)
|
|
android_app->permission_state |= PLAT_ANDROID_PERM_GRANTED;
|
|
looper = android_app->looper;
|
|
scond_broadcast(android_app->cond);
|
|
slock_unlock(android_app->mutex);
|
|
|
|
if (looper)
|
|
ALooper_wake(looper);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Storage permission resolved (granted: %d).\n",
|
|
granted ? 1 : 0);
|
|
}
|
|
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_RetroActivityCommon_safTreeAdded
|
|
(JNIEnv *env, jobject this_obj, jstring tree_obj)
|
|
{
|
|
#ifdef HAVE_SAF
|
|
const char *tree;
|
|
char *serialized_path;
|
|
|
|
tree = (*env)->GetStringUTFChars(env, tree_obj, NULL);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
return;
|
|
}
|
|
|
|
if ((serialized_path = retro_vfs_path_join_saf(tree, "")) != NULL)
|
|
{
|
|
generic_action_ok_displaylist_push(
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_PICKER),
|
|
serialized_path,
|
|
MENU_ENUM_LABEL_FAVORITES_STR,
|
|
MENU_SETTING_ACTION,
|
|
0,
|
|
0,
|
|
ACTION_OK_DL_CONTENT_LIST);
|
|
free(serialized_path);
|
|
}
|
|
|
|
(*env)->ReleaseStringUTFChars(env, tree_obj, tree);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
|
|
android_vfs_authorized_locations_refresh();
|
|
#endif
|
|
}
|
|
|
|
#elif !defined(DINGUX)
|
|
static bool make_proc_acpi_key_val(char **_ptr, char **_key, char **_val)
|
|
{
|
|
char *ptr = *_ptr;
|
|
|
|
while (*ptr == ' ')
|
|
ptr++; /* skip whitespace. */
|
|
|
|
if (*ptr == '\0')
|
|
return false; /* EOF. */
|
|
|
|
*_key = ptr;
|
|
|
|
while ((*ptr != ':') && (*ptr != '\0'))
|
|
ptr++;
|
|
|
|
if (*ptr == '\0')
|
|
return false; /* (unexpected) EOF. */
|
|
|
|
*(ptr++) = '\0'; /* terminate the key. */
|
|
|
|
while (*ptr == ' ')
|
|
ptr++; /* skip whitespace. */
|
|
|
|
if (*ptr == '\0')
|
|
return false; /* (unexpected) EOF. */
|
|
|
|
*_val = ptr;
|
|
|
|
while ((*ptr != '\n') && (*ptr != '\0'))
|
|
ptr++;
|
|
|
|
if (*ptr != '\0')
|
|
*(ptr++) = '\0'; /* terminate the value. */
|
|
|
|
*_ptr = ptr; /* store for next time. */
|
|
return true;
|
|
}
|
|
|
|
static void check_proc_acpi_battery(const char * node, bool * have_battery,
|
|
bool * charging, int *seconds, int *percent)
|
|
{
|
|
char basenode[512];
|
|
char path[PATH_MAX_LENGTH];
|
|
int64_t length = 0;
|
|
char *ptr = NULL;
|
|
char *buf = NULL;
|
|
char *buf_info = NULL;
|
|
char *key = NULL;
|
|
char *val = NULL;
|
|
bool charge = false;
|
|
bool choose = false;
|
|
int maximum = -1;
|
|
int remaining = -1;
|
|
int secs = -1;
|
|
int pct = -1;
|
|
|
|
fill_pathname_join_special(basenode, PROC_ACPI_BATTERY_PATH,
|
|
node, sizeof(basenode));
|
|
fill_pathname_join_special(path, basenode, "state", sizeof(path));
|
|
|
|
if (!filestream_exists(path))
|
|
goto end;
|
|
|
|
if (!filestream_read_file(path, (void**)&buf, &length))
|
|
goto end;
|
|
|
|
fill_pathname_join_special(path, basenode, "info", sizeof(path));
|
|
if (!filestream_read_file(path, (void**)&buf_info, &length))
|
|
goto end;
|
|
|
|
ptr = &buf[0];
|
|
|
|
while (make_proc_acpi_key_val(&ptr, &key, &val))
|
|
{
|
|
if (string_is_equal(key, "present"))
|
|
{
|
|
if (string_is_equal(val, "yes"))
|
|
*have_battery = true;
|
|
}
|
|
else if (string_is_equal(key, "charging state"))
|
|
{
|
|
if (string_is_equal(val, "charging"))
|
|
charge = true;
|
|
else if (string_is_equal(val, "charging/discharging"))
|
|
charge = true;
|
|
}
|
|
else if (string_is_equal(key, "remaining capacity"))
|
|
{
|
|
char *endptr = NULL;
|
|
if (endptr && *endptr == ' ')
|
|
remaining = (int)strtol(val, &endptr, 10);
|
|
}
|
|
}
|
|
|
|
ptr = &buf_info[0];
|
|
|
|
while (make_proc_acpi_key_val(&ptr, &key, &val))
|
|
{
|
|
char *endptr = NULL;
|
|
|
|
if (string_is_equal(key, "design capacity"))
|
|
if (endptr && *endptr == ' ')
|
|
maximum = (int)strtol(val, &endptr, 10);
|
|
}
|
|
|
|
if ((maximum >= 0) && (remaining >= 0))
|
|
{
|
|
pct = (int) ((((float) remaining) / ((float) maximum)) * 100.0f);
|
|
if (pct < 0)
|
|
pct = 0;
|
|
if (pct > 100)
|
|
pct = 100;
|
|
}
|
|
|
|
/* !!! FIXME: calculate (secs). */
|
|
|
|
/*
|
|
* We pick the battery that claims to have the most minutes left.
|
|
* (failing a report of minutes, we'll take the highest percent.)
|
|
*/
|
|
if ((secs < 0) && (*seconds < 0))
|
|
{
|
|
if ((pct < 0) && (*percent < 0))
|
|
choose = true; /* at least we know there's a battery. */
|
|
if (pct > *percent)
|
|
choose = true;
|
|
}
|
|
else if (secs > *seconds)
|
|
choose = true;
|
|
|
|
if (choose)
|
|
{
|
|
*seconds = secs;
|
|
*percent = pct;
|
|
*charging = charge;
|
|
}
|
|
|
|
end:
|
|
if (buf_info)
|
|
free(buf_info);
|
|
if (buf)
|
|
free(buf);
|
|
buf = NULL;
|
|
buf_info = NULL;
|
|
}
|
|
static void check_proc_acpi_sysfs_battery(const char *node,
|
|
bool *have_battery, bool *charging, int *seconds,
|
|
int *percent, int *valid_pct_idx)
|
|
{
|
|
char basenode[512];
|
|
char path[PATH_MAX_LENGTH];
|
|
char *buf = NULL;
|
|
char *ptr = NULL;
|
|
char *key = NULL;
|
|
char *val = NULL;
|
|
bool charge = false;
|
|
bool choose = false;
|
|
unsigned capacity = 0;
|
|
int64_t length = 0;
|
|
int maximum = -1;
|
|
int remaining = -1;
|
|
int secs = -1;
|
|
int pct = -1;
|
|
|
|
/* Stat type. Avoid unknown or device supplies. Missing is considered System. */
|
|
fill_pathname_join_special(basenode, PROC_ACPI_SYSFS_BATTERY_PATH,
|
|
node, sizeof(basenode));
|
|
fill_pathname_join_special(path, basenode, "scope", sizeof(path));
|
|
|
|
if (filestream_exists(path) != 0)
|
|
{
|
|
if (filestream_read_file(path, (void**)&buf, &length) == 1 && buf)
|
|
{
|
|
if (strstr((char*)buf, "Unknown"))
|
|
goto end;
|
|
else if (strstr((char*)buf, "Device"))
|
|
goto end;
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
}
|
|
|
|
fill_pathname_join_special(path, basenode, "type", sizeof(path));
|
|
|
|
if (!filestream_exists(path))
|
|
goto status;
|
|
|
|
if (filestream_read_file(path, (void**)&buf, &length) != 1)
|
|
goto status;
|
|
|
|
if (buf)
|
|
{
|
|
if (strstr((char*)buf, "Battery"))
|
|
*have_battery = true;
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
|
|
status:
|
|
fill_pathname_join_special(path, basenode, "status", sizeof(path));
|
|
|
|
if (!filestream_exists(path))
|
|
return;
|
|
|
|
if (filestream_read_file(path, (void**)&buf, &length) != 1)
|
|
return;
|
|
|
|
if (buf)
|
|
{
|
|
if (strstr((char*)buf, "Discharging"))
|
|
*have_battery = true;
|
|
else if (strstr((char*)buf, "Charging"))
|
|
{
|
|
*have_battery = true;
|
|
*charging = true;
|
|
}
|
|
else if (strstr((char*)buf, "Full"))
|
|
*have_battery = true;
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
|
|
fill_pathname_join_special(path, basenode, "capacity", sizeof(path));
|
|
if (filestream_read_file(path, (void**)&buf, &length) != 1)
|
|
goto end;
|
|
|
|
capacity = atoi(buf);
|
|
|
|
/*
|
|
* Keep record of valid capacities for calculating an average
|
|
* on systems with backup battery supplies.
|
|
*/
|
|
(*valid_pct_idx)++;
|
|
(*percent) += capacity;
|
|
|
|
end:
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
|
|
static void check_proc_acpi_ac_adapter(const char * node, bool *have_ac)
|
|
{
|
|
char basenode[512];
|
|
char path[PATH_MAX_LENGTH];
|
|
char *buf = NULL;
|
|
char *ptr = NULL;
|
|
char *key = NULL;
|
|
char *val = NULL;
|
|
int64_t length = 0;
|
|
|
|
fill_pathname_join_special(basenode, PROC_ACPI_AC_ADAPTER_PATH,
|
|
node, sizeof(basenode));
|
|
fill_pathname_join_special(path, basenode, "state", sizeof(path));
|
|
if (!filestream_exists(path))
|
|
return;
|
|
|
|
if (filestream_read_file(path, (void**)&buf, &length) != 1)
|
|
return;
|
|
|
|
ptr = &buf[0];
|
|
while (make_proc_acpi_key_val(&ptr, &key, &val))
|
|
{
|
|
if (string_is_equal(key, "state") &&
|
|
string_is_equal(val, "on-line"))
|
|
*have_ac = true;
|
|
}
|
|
|
|
if (buf)
|
|
free(buf);
|
|
buf = NULL;
|
|
}
|
|
|
|
static void check_proc_acpi_sysfs_ac_adapter(const char * node, bool *have_ac)
|
|
{
|
|
char path[1024];
|
|
int64_t length = 0;
|
|
char *buf = NULL;
|
|
fill_pathname_join_special(path, PROC_ACPI_SYSFS_AC_ADAPTER_PATH,
|
|
"online", sizeof(path));
|
|
if (!filestream_exists(path))
|
|
return;
|
|
|
|
if (filestream_read_file(path, (void**)&buf, &length) != 1)
|
|
return;
|
|
|
|
if (strchr((char*)buf, '1'))
|
|
*have_ac = true;
|
|
|
|
free(buf);
|
|
}
|
|
|
|
static bool next_string(char **_ptr, char **_str)
|
|
{
|
|
char *ptr = *_ptr;
|
|
|
|
while (*ptr == ' ') /* skip any spaces... */
|
|
ptr++;
|
|
|
|
if (*ptr == '\0')
|
|
return false;
|
|
|
|
while ((*ptr != ' ') && (*ptr != '\n') && (*ptr != '\0'))
|
|
ptr++;
|
|
|
|
if (*ptr != '\0')
|
|
*(ptr++) = '\0';
|
|
|
|
*_ptr = ptr;
|
|
return true;
|
|
}
|
|
|
|
static bool int_string(char *str, int *val)
|
|
{
|
|
char *endptr = NULL;
|
|
if (!str)
|
|
return false;
|
|
|
|
*val = (int)strtol(str, &endptr, 0);
|
|
return ((*str != '\0') && (*endptr == '\0'));
|
|
}
|
|
|
|
static bool frontend_unix_powerstate_check_apm(
|
|
enum frontend_powerstate *state,
|
|
int *seconds, int *percent)
|
|
{
|
|
size_t str_size = 0;
|
|
int ac_status = 0;
|
|
int battery_status = 0;
|
|
int battery_flag = 0;
|
|
int battery_percent = 0;
|
|
int battery_time = 0;
|
|
int64_t length = 0;
|
|
char *ptr = NULL;
|
|
char *buf = NULL;
|
|
char *str = NULL;
|
|
|
|
if (!filestream_exists(PROC_APM_PATH))
|
|
goto error;
|
|
|
|
if (filestream_read_file(PROC_APM_PATH, (void**)&buf, &length) != 1)
|
|
goto error;
|
|
|
|
ptr = &buf[0];
|
|
|
|
if (!next_string(&ptr, &str)) /* driver version */
|
|
goto error;
|
|
if (!next_string(&ptr, &str)) /* BIOS version */
|
|
goto error;
|
|
if (!next_string(&ptr, &str)) /* APM flags */
|
|
goto error;
|
|
|
|
if (!next_string(&ptr, &str)) /* AC line status */
|
|
goto error;
|
|
else if (!int_string(str, &ac_status))
|
|
goto error;
|
|
|
|
if (!next_string(&ptr, &str)) /* battery status */
|
|
goto error;
|
|
else if (!int_string(str, &battery_status))
|
|
goto error;
|
|
|
|
if (!next_string(&ptr, &str)) /* battery flag */
|
|
goto error;
|
|
else if (!int_string(str, &battery_flag))
|
|
goto error;
|
|
if (!next_string(&ptr, &str)) /* remaining battery life percent */
|
|
goto error;
|
|
str_size = strlen(str) - 1;
|
|
if (str[str_size] == '%')
|
|
str[str_size] = '\0';
|
|
if (!int_string(str, &battery_percent))
|
|
goto error;
|
|
|
|
if (!next_string(&ptr, &str)) /* remaining battery life time */
|
|
goto error;
|
|
else if (!int_string(str, &battery_time))
|
|
goto error;
|
|
|
|
if (!next_string(&ptr, &str)) /* remaining battery life time units */
|
|
goto error;
|
|
else if (string_is_equal(str, "min"))
|
|
battery_time *= 60;
|
|
|
|
if (battery_flag == 0xFF) /* unknown state */
|
|
*state = FRONTEND_POWERSTATE_NONE;
|
|
else if (battery_flag & (1 << 7)) /* no battery */
|
|
*state = FRONTEND_POWERSTATE_NO_SOURCE;
|
|
else if (battery_flag & (1 << 3)) /* charging */
|
|
*state = FRONTEND_POWERSTATE_CHARGING;
|
|
else if (ac_status == 1)
|
|
*state = FRONTEND_POWERSTATE_CHARGED; /* on AC, not charging. */
|
|
else
|
|
*state = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
|
|
|
|
if (battery_percent >= 0) /* -1 == unknown */
|
|
*percent = (battery_percent > 100) ? 100 : battery_percent; /* clamp between 0%, 100% */
|
|
if (battery_time >= 0) /* -1 == unknown */
|
|
*seconds = battery_time;
|
|
|
|
if (buf)
|
|
free(buf);
|
|
buf = NULL;
|
|
|
|
return true;
|
|
|
|
error:
|
|
if (buf)
|
|
free(buf);
|
|
buf = NULL;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool frontend_unix_powerstate_check_acpi(
|
|
enum frontend_powerstate *state,
|
|
int *seconds, int *percent)
|
|
{
|
|
bool have_battery = false;
|
|
bool have_ac = false;
|
|
bool charging = false;
|
|
struct RDIR *entry = retro_opendir(PROC_ACPI_BATTERY_PATH);
|
|
if (!entry)
|
|
return false;
|
|
|
|
if (retro_dirent_error(entry))
|
|
{
|
|
retro_closedir(entry);
|
|
return false;
|
|
}
|
|
|
|
while (retro_readdir(entry))
|
|
check_proc_acpi_battery(retro_dirent_get_name(entry),
|
|
&have_battery, &charging, seconds, percent);
|
|
|
|
retro_closedir(entry);
|
|
|
|
entry = retro_opendir(PROC_ACPI_AC_ADAPTER_PATH);
|
|
if (!entry)
|
|
return false;
|
|
|
|
while (retro_readdir(entry))
|
|
check_proc_acpi_ac_adapter(
|
|
retro_dirent_get_name(entry), &have_ac);
|
|
|
|
retro_closedir(entry);
|
|
|
|
if (!have_battery)
|
|
*state = FRONTEND_POWERSTATE_NO_SOURCE;
|
|
else if (charging)
|
|
*state = FRONTEND_POWERSTATE_CHARGING;
|
|
else if (have_ac)
|
|
*state = FRONTEND_POWERSTATE_CHARGED;
|
|
else
|
|
*state = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool frontend_unix_powerstate_check_acpi_sysfs(
|
|
enum frontend_powerstate *state,
|
|
int *seconds, int *percent)
|
|
{
|
|
bool have_battery = false;
|
|
bool have_ac = false;
|
|
bool charging = false;
|
|
int valid_pct_idx = 0;
|
|
struct RDIR *entry = retro_opendir(PROC_ACPI_SYSFS_BATTERY_PATH);
|
|
if (!entry)
|
|
goto error;
|
|
|
|
if (retro_dirent_error(entry))
|
|
goto error;
|
|
|
|
while (retro_readdir(entry))
|
|
{
|
|
const char *node = retro_dirent_get_name(entry);
|
|
|
|
if (node && (strstr(node, "BAT") || strstr(node, "battery")))
|
|
check_proc_acpi_sysfs_battery(node,
|
|
&have_battery, &charging, seconds, percent, &valid_pct_idx);
|
|
}
|
|
|
|
/* Get average percentage */
|
|
if (valid_pct_idx)
|
|
(*percent) /= valid_pct_idx;
|
|
|
|
retro_closedir(entry);
|
|
|
|
if ((entry = retro_opendir(PROC_ACPI_SYSFS_AC_ADAPTER_PATH)))
|
|
{
|
|
check_proc_acpi_sysfs_ac_adapter(retro_dirent_get_name(entry), &have_ac);
|
|
retro_closedir(entry);
|
|
}
|
|
else
|
|
have_ac = false;
|
|
|
|
if (!have_battery)
|
|
*state = FRONTEND_POWERSTATE_NO_SOURCE;
|
|
else if (charging)
|
|
*state = FRONTEND_POWERSTATE_CHARGING;
|
|
else if (have_ac)
|
|
*state = FRONTEND_POWERSTATE_CHARGED;
|
|
else
|
|
*state = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
|
|
|
|
return true;
|
|
|
|
error:
|
|
if (entry)
|
|
retro_closedir(entry);
|
|
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
static enum frontend_powerstate frontend_unix_get_powerstate(
|
|
int *seconds, int *percent)
|
|
{
|
|
enum frontend_powerstate ret = FRONTEND_POWERSTATE_NONE;
|
|
#if defined(ANDROID)
|
|
jint powerstate = FRONTEND_POWERSTATE_NONE;
|
|
jint battery_level = 0;
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (!env || !g_android)
|
|
return FRONTEND_POWERSTATE_NONE;
|
|
|
|
if (g_android->getPowerstate)
|
|
CALL_INT_METHOD(env, powerstate,
|
|
g_android->activity->clazz, g_android->getPowerstate);
|
|
|
|
if (g_android->getBatteryLevel)
|
|
CALL_INT_METHOD(env, battery_level,
|
|
g_android->activity->clazz, g_android->getBatteryLevel);
|
|
|
|
*percent = battery_level;
|
|
|
|
ret = (enum frontend_powerstate)powerstate;
|
|
#elif defined(RETROFW)
|
|
*percent = retrofw_get_battery_level(&ret);
|
|
|
|
/* 'Time left' reporting is unsupported */
|
|
*seconds = -1;
|
|
#elif defined(DINGUX)
|
|
/* Dingux seems to have limited battery
|
|
* reporting capability - if we get a valid
|
|
* integer here, just assume that state is
|
|
* FRONTEND_POWERSTATE_ON_POWER_SOURCE
|
|
* (since most dingux devices are not meant
|
|
* to be used while charging...) */
|
|
int battery_level = dingux_get_battery_level();
|
|
|
|
if (battery_level < 0)
|
|
*percent = -1;
|
|
else
|
|
{
|
|
*percent = battery_level;
|
|
ret = FRONTEND_POWERSTATE_ON_POWER_SOURCE;
|
|
}
|
|
|
|
/* 'Time left' reporting is unsupported */
|
|
*seconds = -1;
|
|
#else
|
|
if (frontend_unix_powerstate_check_acpi_sysfs(&ret, seconds, percent))
|
|
return ret;
|
|
|
|
ret = FRONTEND_POWERSTATE_NONE;
|
|
|
|
if (frontend_unix_powerstate_check_acpi(&ret, seconds, percent))
|
|
return ret;
|
|
|
|
if (frontend_unix_powerstate_check_apm(&ret, seconds, percent))
|
|
return ret;
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
static enum frontend_architecture frontend_unix_get_arch(void)
|
|
{
|
|
struct utsname buffer;
|
|
const char *val = NULL;
|
|
|
|
if (uname(&buffer) != 0)
|
|
return FRONTEND_ARCH_NONE;
|
|
|
|
val = buffer.machine;
|
|
|
|
if (string_is_equal(val, "aarch64"))
|
|
return FRONTEND_ARCH_ARMV8;
|
|
else if (
|
|
string_is_equal(val, "armv7l") ||
|
|
string_is_equal(val, "armv7b")
|
|
)
|
|
return FRONTEND_ARCH_ARMV7;
|
|
else if (string_starts_with_size(val, "arm", STRLEN_CONST("arm")))
|
|
return FRONTEND_ARCH_ARM;
|
|
else if (string_is_equal(val, "x86_64"))
|
|
return FRONTEND_ARCH_X86_64;
|
|
else if (string_is_equal(val, "x86"))
|
|
return FRONTEND_ARCH_X86;
|
|
else if (string_is_equal(val, "ppc64"))
|
|
return FRONTEND_ARCH_PPC;
|
|
else if (string_is_equal(val, "mips"))
|
|
return FRONTEND_ARCH_MIPS;
|
|
else if (string_is_equal(val, "tile"))
|
|
return FRONTEND_ARCH_TILE;
|
|
|
|
return FRONTEND_ARCH_NONE;
|
|
}
|
|
|
|
#ifdef WEBOS
|
|
const char *retroarch_get_webos_version(char *s, size_t len,
|
|
int *major, int *minor)
|
|
{
|
|
static char pretty[128];
|
|
|
|
FILE *f = fopen("/usr/lib/os-release", "r");
|
|
if (!f)
|
|
{
|
|
/* fallback to nyx os_info.json */
|
|
f = fopen("/var/run/nyx/os_info.json", "r");
|
|
if (!f)
|
|
return strlcpy_lit(s, "webOS (unknown)", len), "webOS (unknown)";
|
|
|
|
/* read whole file into buffer */
|
|
char buf[4096];
|
|
size_t n = fread(buf, 1, sizeof(buf)-1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
|
|
rjson_t *json = rjson_open_string(buf, n);
|
|
enum rjson_type t;
|
|
const char *key = NULL, *val = NULL;
|
|
const char *name_str = NULL, *release_str = NULL;
|
|
|
|
while ((t = rjson_next(json)) != RJSON_DONE && t != RJSON_ERROR)
|
|
{
|
|
if (t == RJSON_STRING)
|
|
{
|
|
key = rjson_get_string(json, NULL);
|
|
t = rjson_next(json);
|
|
if (t == RJSON_STRING)
|
|
{
|
|
val = rjson_get_string(json, NULL);
|
|
if (strcmp(key, "webos_name") == 0)
|
|
name_str = val;
|
|
else if (strcmp(key, "webos_release") == 0)
|
|
release_str = val;
|
|
}
|
|
}
|
|
}
|
|
rjson_free(json);
|
|
|
|
if (name_str && release_str)
|
|
snprintf(pretty, sizeof(pretty), "%s %s", name_str, release_str);
|
|
else if (name_str)
|
|
snprintf(pretty, sizeof(pretty), "%s", name_str);
|
|
else
|
|
snprintf(pretty, sizeof(pretty), "webOS (unknown)");
|
|
|
|
if (release_str) {
|
|
char *endptr = NULL;
|
|
long maj = strtol(release_str, &endptr, 10);
|
|
if (major) *major = (int)maj;
|
|
if (endptr && *endptr == '.' && minor)
|
|
*minor = (int)strtol(endptr+1, NULL, 10);
|
|
else if (minor)
|
|
*minor = 0;
|
|
}
|
|
|
|
return strlcpy(s, pretty, len), pretty;
|
|
}
|
|
|
|
char line[256];
|
|
while (fgets(line, sizeof(line), f))
|
|
{
|
|
if (strncmp(line, "PRETTY_NAME=", 12) == 0)
|
|
{
|
|
char *val = line + 12;
|
|
char *nl = strchr(val, '\n');
|
|
if (nl) *nl = '\0';
|
|
if ((val[0] == '"' || val[0] == '\'')) {
|
|
size_t l = strlen(val);
|
|
if (l > 1 && val[l-1] == val[0]) {
|
|
val[l-1] = '\0';
|
|
val++;
|
|
}
|
|
}
|
|
strlcpy(pretty, val, sizeof(pretty));
|
|
}
|
|
else if (strncmp(line, "VERSION_ID=", 11) == 0)
|
|
{
|
|
char *val = line + 11;
|
|
char *nl = strchr(val, '\n');
|
|
if (nl) *nl = '\0';
|
|
char *endptr = NULL;
|
|
long maj = strtol(val, &endptr, 10);
|
|
if (major) *major = (int)maj;
|
|
if (endptr && *endptr == '.' && minor)
|
|
*minor = (int)strtol(endptr+1, NULL, 10);
|
|
else if (minor)
|
|
*minor = 0;
|
|
}
|
|
}
|
|
fclose(f);
|
|
|
|
if (pretty[0] == '\0')
|
|
strlcpy_lit(pretty, "webOS (unknown)", sizeof(pretty));
|
|
|
|
return strlcpy(s, pretty, len), pretty;
|
|
}
|
|
#endif
|
|
|
|
static size_t frontend_unix_get_os(char *s,
|
|
size_t len, int *major, int *minor)
|
|
{
|
|
size_t _len = 0;
|
|
#ifdef ANDROID
|
|
int rel;
|
|
frontend_android_get_version(major, minor, &rel);
|
|
_len = strlcpy_lit(s, "Android", len);
|
|
#else
|
|
char *ptr;
|
|
struct utsname buffer;
|
|
if (uname(&buffer) != 0)
|
|
return _len;
|
|
*major = (int)strtol(buffer.release, &ptr, 10);
|
|
*minor = (int)strtol(++ptr, NULL, 10);
|
|
#if defined(__FreeBSD__)
|
|
_len = strlcpy_lit(s, "FreeBSD", len);
|
|
#elif defined(__NetBSD__)
|
|
_len = strlcpy_lit(s, "NetBSD", len);
|
|
#elif defined(__OpenBSD__)
|
|
_len = strlcpy_lit(s, "OpenBSD", len);
|
|
#elif defined(__DragonFly__)
|
|
_len = strlcpy_lit(s, "DragonFly BSD", len);
|
|
#elif defined(BSD)
|
|
_len = strlcpy_lit(s, "BSD", len);
|
|
#elif defined(__HAIKU__)
|
|
_len = strlcpy_lit(s, "Haiku", len);
|
|
#elif defined(WEBOS)
|
|
_len = strlcpy(s, retroarch_get_webos_version(s, len, major, minor), len);
|
|
#else
|
|
_len = strlcpy_lit(s, "Linux", len);
|
|
#endif
|
|
#endif
|
|
return _len;
|
|
}
|
|
|
|
#ifdef HAVE_LAKKA
|
|
static void frontend_unix_get_lakka_version(char *s,
|
|
size_t len)
|
|
{
|
|
char version[128];
|
|
size_t version_len;
|
|
FILE *command_file = popen("cat /etc/release", "r");
|
|
|
|
fgets(version, sizeof(version), command_file);
|
|
version_len = strlen(version);
|
|
|
|
if (version_len > 0 && version[version_len-1] == '\n')
|
|
version[--version_len] = '\0';
|
|
|
|
strlcpy(s, version, len);
|
|
|
|
pclose(command_file);
|
|
}
|
|
|
|
static void frontend_unix_set_screen_brightness(int value)
|
|
{
|
|
char *buffer = NULL;
|
|
char svalue[16] = {0};
|
|
unsigned int max_brightness = 100;
|
|
/* Device tree should have 'label = "backlight";' if control is desirable */
|
|
filestream_read_file("/sys/class/backlight/backlight/max_brightness",
|
|
(void **)&buffer, NULL);
|
|
if (buffer)
|
|
{
|
|
max_brightness = (unsigned int)strtoul(buffer, NULL, 10);
|
|
free(buffer);
|
|
}
|
|
/* Calculate the brightness */
|
|
value = (value * max_brightness) / 100;
|
|
snprintf(svalue, sizeof(svalue), "%d\n", value);
|
|
filestream_write_file("/sys/class/backlight/backlight/brightness",
|
|
svalue, strlen(svalue));
|
|
}
|
|
#endif
|
|
|
|
static void frontend_unix_get_env(int *argc,
|
|
char *argv[], void *data, void *params_data)
|
|
{
|
|
unsigned i;
|
|
const char* libretro_directory = getenv("LIBRETRO_DIRECTORY");
|
|
const char* libretro_assets_directory = getenv("LIBRETRO_ASSETS_DIRECTORY");
|
|
const char* libretro_autoconfig_directory = getenv("LIBRETRO_AUTOCONFIG_DIRECTORY");
|
|
const char* libretro_cheats_directory = getenv("LIBRETRO_CHEATS_DIRECTORY");
|
|
const char* libretro_database_directory = getenv("LIBRETRO_DATABASE_DIRECTORY");
|
|
const char* libretro_system_directory = getenv("LIBRETRO_SYSTEM_DIRECTORY");
|
|
const char* libretro_video_filter_directory = getenv("LIBRETRO_VIDEO_FILTER_DIRECTORY");
|
|
const char* libretro_video_shader_directory = getenv("LIBRETRO_VIDEO_SHADER_DIRECTORY");
|
|
#ifdef ANDROID
|
|
int32_t major, minor, rel;
|
|
char device_model[PROP_VALUE_MAX] = {0};
|
|
struct rarch_main_wrap *args = NULL;
|
|
JNIEnv *env = NULL;
|
|
jobject obj = NULL;
|
|
jstring jstr = NULL;
|
|
struct android_app *android_app = (struct android_app*)data;
|
|
char parent_path[PATH_MAX_LENGTH];
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
if (!(env = jni_thread_getenv()))
|
|
return;
|
|
|
|
if ((args = (struct rarch_main_wrap*)params_data))
|
|
{
|
|
args->flags &= ~(RARCH_MAIN_WRAP_FLAG_VERBOSE
|
|
| RARCH_MAIN_WRAP_FLAG_NO_CONTENT);
|
|
args->flags |= RARCH_MAIN_WRAP_FLAG_TOUCHED;
|
|
args->sram_path = NULL;
|
|
args->state_path = NULL;
|
|
}
|
|
|
|
frontend_android_get_version(&major, &minor, &rel);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Android version (major : %d, minor : %d, rel : %d)\n",
|
|
major, minor, rel);
|
|
|
|
CALL_OBJ_METHOD(env, obj, android_app->activity->clazz,
|
|
android_app->getIntent);
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Checking arguments passed from intent ...\n");
|
|
|
|
/* Config file. */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "CONFIGFILE"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
if (argv && *argv)
|
|
strlcpy(android_config_path, argv, sizeof(android_config_path));
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Config file: \"%s\".\n", android_config_path);
|
|
if (args && *android_config_path)
|
|
args->config_path = android_config_path;
|
|
}
|
|
|
|
/* Current IME. */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "IME"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
strlcpy(android_app->current_ime, argv,
|
|
sizeof(android_app->current_ime));
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Current IME: \"%s\".\n", android_app->current_ime);
|
|
}
|
|
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "USED"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
bool used = string_is_equal(argv, "false") ? false : true;
|
|
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Used: \"%s\".\n", used ? "true" : "false");
|
|
}
|
|
|
|
/* LIBRETRO. */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "LIBRETRO"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
static char core_path[PATH_MAX_LENGTH];
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
*core_path = '\0';
|
|
if (argv && *argv)
|
|
strlcpy(core_path, argv, sizeof(core_path));
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Libretro path: \"%s\".\n", core_path);
|
|
if (args && *core_path)
|
|
args->libretro_path = core_path;
|
|
}
|
|
|
|
/* Content. */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "ROM"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
static char path[PATH_MAX_LENGTH];
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
*path = '\0';
|
|
|
|
if (argv && *argv)
|
|
strlcpy(path, argv, sizeof(path));
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
if (*path)
|
|
{
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Auto-start game \"%s\".\n", path);
|
|
if (args && *path)
|
|
args->content_path = path;
|
|
}
|
|
}
|
|
|
|
/* Internal Storage */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "SDCARD"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
internal_storage_path[0] = '\0';
|
|
|
|
if (argv && *argv)
|
|
strlcpy(internal_storage_path, argv,
|
|
sizeof(internal_storage_path));
|
|
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
if (*internal_storage_path)
|
|
{
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Android internal storage location: \"%s\".\n",
|
|
internal_storage_path);
|
|
}
|
|
}
|
|
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "APK"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
*apk_dir = '\0';
|
|
|
|
if (argv && *argv)
|
|
strlcpy(apk_dir, argv, sizeof(apk_dir));
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
if (*apk_dir)
|
|
{
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] APK location \"%s\".\n", apk_dir);
|
|
}
|
|
}
|
|
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "EXTERNAL"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
*internal_storage_app_path = '\0';
|
|
|
|
if (argv && *argv)
|
|
strlcpy(internal_storage_app_path, argv,
|
|
sizeof(internal_storage_app_path));
|
|
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
if (*internal_storage_app_path)
|
|
{
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Android external files location \"%s\".\n",
|
|
internal_storage_app_path);
|
|
}
|
|
}
|
|
|
|
/* Device-optimal audio output parameters, queried from
|
|
* AudioManager on the Java side. These become the defaults;
|
|
* values saved in the config file still win. */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "AUDIO_RATE"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
int rate = 0;
|
|
|
|
if (argv && *argv)
|
|
rate = atoi(argv);
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
if (rate > 0)
|
|
{
|
|
g_defaults.settings_out_sample_rate = rate;
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Device sample rate: %d Hz.\n", rate);
|
|
}
|
|
}
|
|
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "AUDIO_FRAMES"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
int frames = 0;
|
|
|
|
if (argv && *argv)
|
|
frames = atoi(argv);
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
if (frames > 0)
|
|
{
|
|
g_defaults.settings_out_block_frames = frames;
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Device audio block frames: %d.\n", frames);
|
|
}
|
|
}
|
|
|
|
/* Content. */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "DATADIR"));
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
*app_dir = '\0';
|
|
|
|
if (argv && *argv)
|
|
strlcpy(app_dir, argv, sizeof(app_dir));
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] App dir: \"%s\".\n", app_dir);
|
|
}
|
|
|
|
/* Anything the intent did not supply is derived from the
|
|
* application context, so a direct launch of the native activity
|
|
* behaves like one through the Java launcher. Extras always win:
|
|
* every derivation below runs only for a value still unset. */
|
|
if (!*app_dir || !*apk_dir)
|
|
android_env_derive_application_info(env, android_app->activity->clazz);
|
|
if (!*internal_storage_path || !*internal_storage_app_path)
|
|
android_env_derive_storage(env, android_app->activity->clazz);
|
|
if (!*android_app->current_ime)
|
|
android_env_derive_ime(env, android_app->activity->clazz,
|
|
android_app->current_ime, sizeof(android_app->current_ime));
|
|
if ( g_defaults.settings_out_sample_rate <= 0
|
|
|| g_defaults.settings_out_block_frames <= 0)
|
|
android_env_derive_audio(env, android_app->activity->clazz);
|
|
if (args && !args->config_path)
|
|
{
|
|
android_env_derive_config_path(env, android_app->activity->clazz);
|
|
if (*android_config_path)
|
|
{
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Derived config file: \"%s\".\n",
|
|
android_config_path);
|
|
args->config_path = android_config_path;
|
|
}
|
|
}
|
|
|
|
{
|
|
/* set paths depending on the ability to write
|
|
* to internal_storage_path */
|
|
|
|
if (*internal_storage_path &&
|
|
test_permissions_android(internal_storage_path))
|
|
{
|
|
storage_permissions = INTERNAL_STORAGE_WRITABLE;
|
|
}
|
|
else if (*internal_storage_app_path &&
|
|
test_permissions_android(internal_storage_app_path))
|
|
{
|
|
storage_permissions = INTERNAL_STORAGE_APPDIR_WRITABLE;
|
|
}
|
|
else
|
|
{
|
|
// fallback to private data storage
|
|
// e.g. /data/user/0/com.retroarch.aarch64 then saves/ etc.
|
|
storage_permissions = INTERNAL_STORAGE_NOT_WRITABLE;
|
|
}
|
|
|
|
/* code to populate default paths*/
|
|
if (*app_dir)
|
|
{
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Application location: \"%s\".\n", app_dir);
|
|
if (args && *app_dir)
|
|
{
|
|
|
|
/* this section populates the paths for the assets that are bundled
|
|
with the APK.
|
|
TODO/FIXME: change the extraction method so it honors the user defined paths instead
|
|
*/
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS], app_dir,
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SHADER], app_dir,
|
|
"shaders", sizeof(g_defaults.dirs[DEFAULT_DIR_SHADER]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OVERLAY], app_dir,
|
|
"overlays", sizeof(g_defaults.dirs[DEFAULT_DIR_OVERLAY]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY], app_dir,
|
|
"overlays/keyboards", sizeof(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY]));
|
|
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE], app_dir,
|
|
"cores", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_INFO],
|
|
app_dir, "info",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG],
|
|
app_dir, "autoconfig",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER],
|
|
app_dir, "filters/audio",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
app_dir, "filters/video",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_CONTENT_HISTORY],
|
|
app_dir, sizeof(g_defaults.dirs[DEFAULT_DIR_CONTENT_HISTORY]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_DATABASE],
|
|
app_dir, "database/rdb",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_DATABASE]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_WALLPAPERS],
|
|
app_dir, "assets/wallpapers",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_WALLPAPERS]));
|
|
|
|
/* This switch tries to handle the different locations for devices with
|
|
weird write permissions. Should be largelly unnecessary nowadays. Most
|
|
devices I have tested are INTERNAL_STORAGE_WRITABLE but better safe than sorry */
|
|
|
|
switch (storage_permissions)
|
|
{
|
|
/* only /sdcard/Android/data/com.retroarch is writable */
|
|
case INTERNAL_STORAGE_APPDIR_WRITABLE:
|
|
strlcpy(parent_path, internal_storage_app_path, sizeof(parent_path));
|
|
break;
|
|
/* only the internal app dir is writable, this should never happen but it did
|
|
a few years ago in some devices */
|
|
case INTERNAL_STORAGE_NOT_WRITABLE:
|
|
strlcpy(parent_path, app_dir, sizeof(parent_path));
|
|
break;
|
|
/* sdcard is writable, this should be the case most of the time*/
|
|
case INTERNAL_STORAGE_WRITABLE:
|
|
fill_pathname_join(parent_path,
|
|
internal_storage_path, "RetroArch",
|
|
sizeof(parent_path));
|
|
break;
|
|
}
|
|
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SRAM],
|
|
parent_path, "saves",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SRAM]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SAVESTATE],
|
|
parent_path, "states",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SAVESTATE]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SYSTEM],
|
|
parent_path, "system",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SYSTEM]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SCREENSHOT],
|
|
parent_path, "screenshots",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SCREENSHOT]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS],
|
|
parent_path, "downloads",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS]));
|
|
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_LOGS],
|
|
parent_path, "logs",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_LOGS]));
|
|
|
|
/* remaps is nested in config */
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG],
|
|
parent_path, "config",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_REMAP],
|
|
g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG], "remaps",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_REMAP]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_THUMBNAILS],
|
|
parent_path, "thumbnails",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_THUMBNAILS]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_PLAYLIST],
|
|
parent_path, "playlists",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_PLAYLIST]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CHEATS],
|
|
parent_path, "cheats",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CHEATS]));
|
|
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CACHE],
|
|
parent_path, "temp",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CACHE]));
|
|
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Default savefile folder: \"%s\".",
|
|
g_defaults.dirs[DEFAULT_DIR_SRAM]);
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Default savestate folder: \"%s\".",
|
|
g_defaults.dirs[DEFAULT_DIR_SAVESTATE]);
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Default system folder: \"%s\".",
|
|
g_defaults.dirs[DEFAULT_DIR_SYSTEM]);
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] Default screenshot folder: \"%s\".",
|
|
g_defaults.dirs[DEFAULT_DIR_SCREENSHOT]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Bundle asset extraction parameters. The APK is the source
|
|
* archive, the app data dir the destination, and the version
|
|
* code gates extraction to app upgrades (see menu_driver.c).
|
|
* These settings are intentionally not bound to the config
|
|
* file on Android (see configuration.c). */
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, obj, android_app->getStringExtra,
|
|
(*env)->NewStringUTF(env, "VERSIONCODE"));
|
|
|
|
{
|
|
settings_t *settings = config_get_ptr();
|
|
unsigned version = 0;
|
|
|
|
if (android_app->getStringExtra && jstr)
|
|
{
|
|
const char *argv = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
if (argv && *argv)
|
|
version = (unsigned)strtoul(argv, NULL, 10);
|
|
(*env)->ReleaseStringUTFChars(env, jstr, argv);
|
|
}
|
|
if (!version)
|
|
version = android_env_derive_version_code(env,
|
|
android_app->activity->clazz);
|
|
|
|
if (version && settings && *apk_dir && *app_dir)
|
|
{
|
|
configuration_set_string(settings,
|
|
settings->paths.bundle_assets_src, apk_dir);
|
|
configuration_set_string(settings,
|
|
settings->paths.bundle_assets_dst, app_dir);
|
|
configuration_set_string(settings,
|
|
settings->paths.bundle_assets_dst_subdir, "assets");
|
|
configuration_set_uint(settings,
|
|
settings->uints.bundle_assets_extract_version_current,
|
|
version);
|
|
__android_log_print(ANDROID_LOG_INFO,
|
|
"RetroArch", "[ENV] App version code: %u.\n", version);
|
|
}
|
|
}
|
|
|
|
system_property_get("getprop", "ro.product.model", device_model, sizeof(device_model));
|
|
|
|
/* Set automatic default values per device */
|
|
if (g_platform_android_flags & PLAT_ANDROID_FLAG_XPERIA_PLAY_DEVICE)
|
|
g_defaults.settings_out_latency = 128;
|
|
else if (strstr(device_model, "GAMEMID_BT"))
|
|
g_defaults.settings_out_latency = 160;
|
|
else if (strstr(device_model, "SHIELD"))
|
|
{
|
|
g_defaults.settings_video_refresh_rate = 60.0;
|
|
#ifdef HAVE_MENU
|
|
#ifdef HAVE_MATERIALUI
|
|
g_defaults.menu_materialui_menu_color_theme_enable = true;
|
|
g_defaults.menu_materialui_menu_color_theme = MATERIALUI_THEME_NVIDIA_SHIELD;
|
|
#endif
|
|
#endif
|
|
}
|
|
else if (strstr(device_model, "JSS15J"))
|
|
g_defaults.settings_video_refresh_rate = 59.65;
|
|
|
|
/* For gamepad-like/console devices:
|
|
*
|
|
* - Explicitly disable input overlay by default
|
|
* - Use Ozone menu driver by default
|
|
*
|
|
* */
|
|
|
|
if ( (g_platform_android_flags & PLAT_ANDROID_FLAG_GAME_CONSOLE_DEVICE)
|
|
|| (g_platform_android_flags & PLAT_ANDROID_FLAG_ANDROID_TV_DEVICE))
|
|
{
|
|
g_defaults.overlay_set = true;
|
|
g_defaults.overlay_enable = false;
|
|
strlcpy_lit(g_defaults.settings_menu, "ozone", sizeof(g_defaults.settings_menu));
|
|
}
|
|
#else
|
|
char base_path[PATH_MAX] = {0};
|
|
#if defined(RARCH_UNIX_CWD_ENV)
|
|
/* The entire path is zero initialized. */
|
|
getcwd(base_path, sizeof(base_path));
|
|
#elif defined(DINGUX)
|
|
dingux_get_base_path(base_path, sizeof(base_path));
|
|
#else
|
|
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
const char *home = getenv("HOME");
|
|
|
|
if (xdg)
|
|
{
|
|
size_t _len = strlcpy(base_path, xdg, sizeof(base_path));
|
|
strlcpy_lit(base_path + _len, "/retroarch", sizeof(base_path) - _len);
|
|
}
|
|
else if (home)
|
|
{
|
|
size_t _len = strlcpy(base_path, home, sizeof(base_path));
|
|
strlcpy_lit(base_path + _len, "/.config/retroarch", sizeof(base_path) - _len);
|
|
}
|
|
else
|
|
strlcpy_lit(base_path, "retroarch", sizeof(base_path));
|
|
#endif
|
|
|
|
if (libretro_directory && *libretro_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_CORE], libretro_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CORE]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE], base_path,
|
|
"cores", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE]));
|
|
#if defined(DINGUX)
|
|
/* On platforms that require manual core installation/
|
|
* removal, placing core info files in the same directory
|
|
* as the cores themselves makes file management highly
|
|
* inconvenient. Use a dedicated core info directory instead */
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_INFO], base_path,
|
|
"core_info", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
|
#else
|
|
#ifdef CORE_INFO_DIR
|
|
if (path_is_directory(CORE_INFO_DIR "/cores"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_INFO], CORE_INFO_DIR,
|
|
"cores", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
|
else
|
|
#endif
|
|
if (libretro_directory && *libretro_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_CORE_INFO], libretro_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_INFO], base_path,
|
|
"cores", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]));
|
|
#endif
|
|
if (libretro_autoconfig_directory && *libretro_autoconfig_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG],
|
|
libretro_autoconfig_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG], base_path,
|
|
"autoconfig", sizeof(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG]));
|
|
#ifdef ASSETS_DIR
|
|
if (path_is_directory(ASSETS_DIR "/assets"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS],
|
|
ASSETS_DIR,
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
else
|
|
#endif
|
|
if (libretro_assets_directory && *libretro_assets_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_ASSETS], libretro_assets_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
else if (path_is_directory("/usr/local/share/retroarch/assets"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS],
|
|
"/usr/local/share/retroarch",
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
else if (path_is_directory("/usr/share/retroarch/assets"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS],
|
|
"/usr/share/retroarch",
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
else if (path_is_directory("/usr/local/share/games/retroarch/assets"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS],
|
|
"/usr/local/share/games/retroarch",
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
else if (path_is_directory("/usr/share/games/retroarch/assets"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS],
|
|
"/usr/share/games/retroarch",
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_ASSETS], base_path,
|
|
"assets", sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]));
|
|
|
|
#if defined(DINGUX)
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER], base_path,
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER], base_path,
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
#else
|
|
#ifdef FILTERS_DIR
|
|
if (path_is_directory(FILTERS_DIR "/filters/audio"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER],
|
|
FILTERS_DIR,
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
else
|
|
#endif
|
|
if (path_is_directory("/usr/local/share/retroarch/filters/audio"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER],
|
|
"/usr/local/share/retroarch",
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
else if (path_is_directory("/usr/share/retroarch/filters/audio"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER],
|
|
"/usr/share/retroarch",
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
else if (path_is_directory("/usr/local/share/games/retroarch/filters/audio"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER],
|
|
"/usr/local/share/games/retroarch",
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
else if (path_is_directory("/usr/share/games/retroarch/filters/audio"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER],
|
|
"/usr/share/games/retroarch",
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER], base_path,
|
|
"filters/audio", sizeof(g_defaults.dirs[DEFAULT_DIR_AUDIO_FILTER]));
|
|
|
|
#ifdef FILTERS_DIR
|
|
if (path_is_directory(FILTERS_DIR "/filters/video"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
FILTERS_DIR,
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
else
|
|
#endif
|
|
if (libretro_video_filter_directory && *libretro_video_filter_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
libretro_video_filter_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
else if (path_is_directory("/usr/local/share/retroarch/filters/video"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
"/usr/local/share/retroarch",
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
else if (path_is_directory("/usr/share/retroarch/filters/video"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
"/usr/share/retroarch",
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
else if (path_is_directory("/usr/local/share/games/retroarch/filters/video"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
"/usr/local/share/games/retroarch",
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
else if (path_is_directory("/usr/share/games/retroarch/filters/video"))
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER],
|
|
"/usr/share/games/retroarch",
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER], base_path,
|
|
"filters/video", sizeof(g_defaults.dirs[DEFAULT_DIR_VIDEO_FILTER]));
|
|
#endif
|
|
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG], base_path,
|
|
"config", sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_REMAP],
|
|
g_defaults.dirs[DEFAULT_DIR_MENU_CONFIG],
|
|
"remaps", sizeof(g_defaults.dirs[DEFAULT_DIR_REMAP]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_PLAYLIST], base_path,
|
|
"playlists", sizeof(g_defaults.dirs[DEFAULT_DIR_PLAYLIST]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_RECORD_CONFIG], base_path,
|
|
"records_config", sizeof(g_defaults.dirs[DEFAULT_DIR_RECORD_CONFIG]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_RECORD_OUTPUT], base_path,
|
|
"records", sizeof(g_defaults.dirs[DEFAULT_DIR_RECORD_OUTPUT]));
|
|
if (libretro_database_directory && *libretro_database_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_DATABASE],
|
|
libretro_database_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_DATABASE]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_DATABASE], base_path,
|
|
"database/rdb", sizeof(g_defaults.dirs[DEFAULT_DIR_DATABASE]));
|
|
if (libretro_video_shader_directory && *libretro_video_shader_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_SHADER],
|
|
libretro_video_shader_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SHADER]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SHADER], base_path,
|
|
"shaders", sizeof(g_defaults.dirs[DEFAULT_DIR_SHADER]));
|
|
if (libretro_cheats_directory && *libretro_cheats_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_CHEATS],
|
|
libretro_cheats_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CHEATS]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CHEATS], base_path,
|
|
"cheats", sizeof(g_defaults.dirs[DEFAULT_DIR_CHEATS]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OVERLAY], base_path,
|
|
"overlays", sizeof(g_defaults.dirs[DEFAULT_DIR_OVERLAY]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY], base_path,
|
|
"overlays/keyboards", sizeof(g_defaults.dirs[DEFAULT_DIR_OSK_OVERLAY]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS], base_path,
|
|
"downloads", sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_ASSETS]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SCREENSHOT], base_path,
|
|
"screenshots", sizeof(g_defaults.dirs[DEFAULT_DIR_SCREENSHOT]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_THUMBNAILS], base_path,
|
|
"thumbnails", sizeof(g_defaults.dirs[DEFAULT_DIR_THUMBNAILS]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_LOGS], base_path,
|
|
"logs", sizeof(g_defaults.dirs[DEFAULT_DIR_LOGS]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SRAM], base_path,
|
|
"saves", sizeof(g_defaults.dirs[DEFAULT_DIR_SRAM]));
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SAVESTATE], base_path,
|
|
"states", sizeof(g_defaults.dirs[DEFAULT_DIR_SAVESTATE]));
|
|
if (libretro_system_directory && *libretro_system_directory)
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_SYSTEM],
|
|
libretro_system_directory,
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SYSTEM]));
|
|
else
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_SYSTEM], base_path,
|
|
"system", sizeof(g_defaults.dirs[DEFAULT_DIR_SYSTEM]));
|
|
|
|
if (test_permissions("/tmp") && path_mkdir("/tmp/retroarch-tmp"))
|
|
{
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CACHE], "/tmp",
|
|
"retroarch-tmp", sizeof(g_defaults.dirs[DEFAULT_DIR_CACHE]));
|
|
}
|
|
else
|
|
{
|
|
fill_pathname_join(g_defaults.dirs[DEFAULT_DIR_CACHE], base_path,
|
|
"temp", sizeof(g_defaults.dirs[DEFAULT_DIR_CACHE]));
|
|
}
|
|
#endif
|
|
|
|
#ifndef IS_SALAMANDER
|
|
#if defined(ANDROID)
|
|
dir_check_defaults("host0:app/custom.ini");
|
|
#else
|
|
dir_check_defaults("custom.ini");
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
#ifdef ANDROID
|
|
static void free_saved_state(struct android_app* android_app)
|
|
{
|
|
slock_lock(android_app->mutex);
|
|
|
|
if (android_app->savedState)
|
|
{
|
|
free(android_app->savedState);
|
|
android_app->savedState = NULL;
|
|
android_app->savedStateSize = 0;
|
|
}
|
|
|
|
slock_unlock(android_app->mutex);
|
|
}
|
|
|
|
static void android_app_destroy(struct android_app *android_app)
|
|
{
|
|
JNIEnv *env = NULL;
|
|
|
|
#ifdef HAVE_ANDROID_LIFECYCLE_HOOKS
|
|
android_run_lifecycle_hook(android_app, "reset");
|
|
#endif
|
|
|
|
free_saved_state(android_app);
|
|
|
|
slock_lock(android_app->mutex);
|
|
|
|
env = jni_thread_getenv();
|
|
|
|
/* onRetroArchExit() calls finish() on the activity. When the destroy
|
|
* originated from the framework we are already inside onDestroy(),
|
|
* so asking it to finish again is at best redundant. */
|
|
if ( env
|
|
&& android_app->onRetroArchExit
|
|
&& !android_app->destroy_from_framework)
|
|
CALL_VOID_METHOD(env, android_app->activity->clazz,
|
|
android_app->onRetroArchExit);
|
|
|
|
#ifdef HAVE_SAF
|
|
android_vfs_authorized_locations_free();
|
|
|
|
if (android_app->have_saf)
|
|
retro_vfs_deinit_saf();
|
|
#endif
|
|
|
|
if (android_app->inputQueue)
|
|
AInputQueue_detachLooper(android_app->inputQueue);
|
|
|
|
AConfiguration_delete(android_app->config);
|
|
android_app->destroyed = 1;
|
|
scond_broadcast(android_app->cond);
|
|
slock_unlock(android_app->mutex);
|
|
/* Can't touch android_app object after this. */
|
|
}
|
|
#endif
|
|
|
|
static bool frontend_unix_set_gamemode(bool on)
|
|
{
|
|
#ifdef FERAL_GAMEMODE
|
|
/* Once gamemode_query_status() reports failure (typically because
|
|
* libgamemode.so is not installed), there is no point repeatedly
|
|
* re-probing on every config load or menu toggle - the result will
|
|
* not change for the lifetime of the process, and each probe emits
|
|
* a warning. Latch the unavailable state and short-circuit. */
|
|
static bool gamemode_unavailable = false;
|
|
int gamemode_status;
|
|
bool gamemode_active;
|
|
|
|
if (gamemode_unavailable)
|
|
return false;
|
|
|
|
gamemode_status = gamemode_query_status();
|
|
gamemode_active = (gamemode_status == 2);
|
|
|
|
if (gamemode_status < 0)
|
|
{
|
|
if (on)
|
|
RARCH_WARN("[GameMode] GameMode cannot be enabled on this system (\"%s.\") "
|
|
"https://github.com/FeralInteractive/gamemode needs to be installed.\n",
|
|
gamemode_error_string());
|
|
|
|
gamemode_unavailable = true;
|
|
return false;
|
|
}
|
|
|
|
if (gamemode_active == on)
|
|
return true;
|
|
|
|
if (on)
|
|
{
|
|
if (gamemode_request_start() != 0)
|
|
{
|
|
RARCH_WARN("[GameMode] Failed to enter GameMode: %s.\n", gamemode_error_string());
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (gamemode_request_end() != 0)
|
|
{
|
|
RARCH_WARN("[GameMode] Failed to exit GameMode: %s.\n", gamemode_error_string());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
static void frontend_unix_deinit(void *data)
|
|
{
|
|
settings_t *settings = config_get_ptr();
|
|
#ifdef ANDROID
|
|
struct android_app *android_app = (struct android_app*)data;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
android_app_destroy(android_app);
|
|
#endif
|
|
|
|
#ifdef HAVE_LAKKA
|
|
/* Reset brightness to maximum */
|
|
if (settings->uints.screen_brightness != DEFAULT_SCREEN_BRIGHTNESS)
|
|
frontend_unix_set_screen_brightness(DEFAULT_SCREEN_BRIGHTNESS);
|
|
#endif
|
|
|
|
frontend_unix_set_gamemode(false);
|
|
}
|
|
|
|
static void frontend_unix_init(void *data)
|
|
{
|
|
#ifdef ANDROID
|
|
char device_model[PROP_VALUE_MAX] = {0};
|
|
JNIEnv *env = NULL;
|
|
ALooper *looper = NULL;
|
|
jboolean jbool = JNI_FALSE;
|
|
jclass class = NULL;
|
|
jobject obj = NULL;
|
|
struct android_app* android_app = (struct android_app*)data;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
android_app->config = AConfiguration_new();
|
|
AConfiguration_fromAssetManager(android_app->config,
|
|
android_app->activity->assetManager);
|
|
|
|
looper = (ALooper*)ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
|
|
ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN,
|
|
ALOOPER_EVENT_INPUT, NULL, NULL);
|
|
slock_lock(android_app->mutex);
|
|
android_app->looper = looper;
|
|
android_app->running = 1;
|
|
scond_broadcast(android_app->cond);
|
|
slock_unlock(android_app->mutex);
|
|
|
|
memset(&g_android, 0, sizeof(g_android));
|
|
g_android = (struct android_app*)android_app;
|
|
|
|
while (!android_app->window)
|
|
{
|
|
if (!android_run_events(android_app))
|
|
{
|
|
frontend_unix_deinit(android_app);
|
|
frontend_android_shutdown(android_app);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Storage-permission gate: filesystem-dependent startup - config
|
|
* probing, asset extraction, storage tests - must not run before
|
|
* the Java side has resolved the runtime permission state. The
|
|
* wait pumps lifecycle commands, so a pause or rotation while a
|
|
* permission dialog is up cannot block the UI thread's
|
|
* synchronized window teardown (ANR). A launch with the
|
|
* permission already settled signals before this loop is reached
|
|
* and never waits. */
|
|
for (;;)
|
|
{
|
|
bool resolved;
|
|
|
|
slock_lock(android_app->mutex);
|
|
resolved = (android_app->permission_state
|
|
& PLAT_ANDROID_PERM_RESOLVED) != 0;
|
|
slock_unlock(android_app->mutex);
|
|
if (resolved)
|
|
break;
|
|
|
|
if (!android_run_events(android_app))
|
|
{
|
|
frontend_unix_deinit(android_app);
|
|
frontend_android_shutdown(android_app);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!(env = jni_thread_getenv()))
|
|
return;
|
|
|
|
GET_OBJECT_CLASS(env, class, android_app->activity->clazz);
|
|
GET_METHOD_ID(env, android_app->getIntent, class,
|
|
"getIntent", "()Landroid/content/Intent;");
|
|
GET_METHOD_ID(env, android_app->onRetroArchExit, class,
|
|
"onRetroArchExit", "()V");
|
|
GET_METHOD_ID(env, android_app->isAndroidTV, class,
|
|
"isAndroidTV", "()Z");
|
|
GET_METHOD_ID(env, android_app->getRefreshRate, class,
|
|
"getRefreshRate", "()F");
|
|
GET_METHOD_ID(env, android_app->getDisplayModes, class,
|
|
"getDisplayModes", "()[I");
|
|
GET_METHOD_ID(env, android_app->getCurrentDisplayModeId, class,
|
|
"getCurrentDisplayModeId", "()I");
|
|
GET_METHOD_ID(env, android_app->setDisplayModeId, class,
|
|
"setDisplayModeId", "(I)Z");
|
|
GET_METHOD_ID(env, android_app->getPowerstate, class,
|
|
"getPowerstate", "()I");
|
|
GET_METHOD_ID(env, android_app->getBatteryLevel, class,
|
|
"getBatteryLevel", "()I");
|
|
GET_METHOD_ID(env, android_app->setSustainedPerformanceMode, class,
|
|
"setSustainedPerformanceMode", "(Z)V");
|
|
GET_METHOD_ID(env, android_app->setWindowSettings, class,
|
|
"setWindowSettings", "(ZZ)V");
|
|
GET_METHOD_ID(env, android_app->setScreenOrientation, class,
|
|
"setScreenOrientation", "(I)V");
|
|
GET_METHOD_ID(env, android_app->doVibrate, class,
|
|
"doVibrate", "(IIII)V");
|
|
GET_METHOD_ID(env, android_app->doVibrateJoypad, class,
|
|
"doVibrateJoypad", "(IIII)V");
|
|
GET_METHOD_ID(env, android_app->doVibrateUSB, class,
|
|
"doVibrateUSB", "(III)Z");
|
|
GET_METHOD_ID(env, android_app->doHapticFeedback, class,
|
|
"doHapticFeedback", "(I)V");
|
|
GET_METHOD_ID(env, android_app->getUserLanguageString, class,
|
|
"getUserLanguageString", "()Ljava/lang/String;");
|
|
GET_METHOD_ID(env, android_app->isPlayStoreBuild, class,
|
|
"isPlayStoreBuild", "()Z");
|
|
GET_METHOD_ID(env, android_app->getAvailableCores, class,
|
|
"getAvailableCores", "()[Ljava/lang/String;");
|
|
GET_METHOD_ID(env, android_app->getInstalledCores, class,
|
|
"getInstalledCores", "()[Ljava/lang/String;");
|
|
GET_METHOD_ID(env, android_app->downloadCore, class,
|
|
"downloadCore", "(Ljava/lang/String;)V");
|
|
GET_METHOD_ID(env, android_app->deleteCore, class,
|
|
"deleteCore", "(Ljava/lang/String;)V");
|
|
CALL_OBJ_METHOD(env, obj, android_app->activity->clazz,
|
|
android_app->getIntent);
|
|
GET_METHOD_ID(env, android_app->getVolumeCount, class,
|
|
"getVolumeCount", "()I");
|
|
GET_METHOD_ID(env, android_app->getVolumePath, class,
|
|
"getVolumePath", "(Ljava/lang/String;)Ljava/lang/String;");
|
|
GET_METHOD_ID(env, android_app->inputGrabMouse, class,
|
|
"inputGrabMouse", "(Z)V");
|
|
GET_METHOD_ID(env, android_app->isScreenReaderEnabled, class,
|
|
"isScreenReaderEnabled", "()Z");
|
|
GET_METHOD_ID(env, android_app->accessibilitySpeak, class,
|
|
"accessibilitySpeak", "(Ljava/lang/String;)V");
|
|
GET_METHOD_ID(env, android_app->showKeyboard, class,
|
|
"showKeyboard", "(Ljava/lang/String;Ljava/lang/String;)V");
|
|
GET_METHOD_ID(env, android_app->hideKeyboard, class,
|
|
"hideKeyboard", "()V");
|
|
|
|
CALL_BOOLEAN_METHOD(env, android_app->is_play_store_build,
|
|
android_app->activity->clazz, android_app->isPlayStoreBuild);
|
|
|
|
#ifdef HAVE_SAF
|
|
GET_METHOD_ID(env, android_app->requestOpenDocumentTree, class,
|
|
"requestOpenDocumentTree", "()V");
|
|
|
|
GET_METHOD_ID(env, android_app->getPersistedSafTrees, class,
|
|
"getPersistedSafTrees", "()[Ljava/lang/String;");
|
|
|
|
android_app->have_saf = retro_vfs_init_saf(jni_thread_getenv, android_app->activity->clazz);
|
|
|
|
if (android_app->have_saf)
|
|
android_vfs_authorized_locations_refresh();
|
|
#endif
|
|
|
|
GET_OBJECT_CLASS(env, class, obj);
|
|
GET_METHOD_ID(env, android_app->getStringExtra, class,
|
|
"getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;");
|
|
|
|
/* Check if we are an Android TV device */
|
|
if (env && android_app->isAndroidTV)
|
|
{
|
|
CALL_BOOLEAN_METHOD(env, jbool,
|
|
android_app->activity->clazz, android_app->isAndroidTV);
|
|
|
|
if (jbool != JNI_FALSE)
|
|
g_platform_android_flags |= PLAT_ANDROID_FLAG_ANDROID_TV_DEVICE;
|
|
}
|
|
|
|
system_property_get("getprop", "ro.product.model", device_model, sizeof(device_model));
|
|
|
|
/* Check if we are a game console device */
|
|
if (device_is_game_console(device_model))
|
|
g_platform_android_flags |= PLAT_ANDROID_FLAG_GAME_CONSOLE_DEVICE;
|
|
|
|
/* Set automatic default values per device */
|
|
if (device_is_xperia_play(device_model))
|
|
g_platform_android_flags |= PLAT_ANDROID_FLAG_XPERIA_PLAY_DEVICE;
|
|
#endif
|
|
}
|
|
|
|
static int frontend_unix_parse_drive_list(void *data, bool load_content)
|
|
{
|
|
#ifdef HAVE_MENU
|
|
file_list_t *list = (file_list_t*)data;
|
|
enum msg_hash_enums enum_idx = load_content ?
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR :
|
|
MENU_ENUM_LABEL_FILE_BROWSER_DIRECTORY;
|
|
|
|
#ifdef ANDROID
|
|
|
|
JNIEnv *env = jni_thread_getenv();
|
|
jint output = 0;
|
|
jstring jstr = NULL;
|
|
|
|
int volume_count = 0;
|
|
/* The shared-storage path already appended below, so the volume
|
|
* loop does not list the primary volume a second time. */
|
|
const char *listed_storage_path = "";
|
|
|
|
if (!env || !g_android)
|
|
return 0;
|
|
|
|
if (!g_android->is_play_store_build && g_android->getVolumeCount)
|
|
{
|
|
CALL_INT_METHOD(env, output,
|
|
g_android->activity->clazz, g_android->getVolumeCount);
|
|
volume_count = output;
|
|
}
|
|
|
|
if (!g_android->is_play_store_build)
|
|
{
|
|
if (*internal_storage_path)
|
|
{
|
|
if (storage_permissions == INTERNAL_STORAGE_WRITABLE)
|
|
{
|
|
char user_data_path[PATH_MAX_LENGTH];
|
|
fill_pathname_join_special(user_data_path,
|
|
internal_storage_path, "RetroArch",
|
|
sizeof(user_data_path));
|
|
|
|
menu_entries_append(list,
|
|
user_data_path,
|
|
msg_hash_to_str(MSG_INTERNAL_STORAGE),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
|
|
menu_entries_append(list,
|
|
internal_storage_path,
|
|
msg_hash_to_str(MSG_INTERNAL_STORAGE),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
listed_storage_path = internal_storage_path;
|
|
}
|
|
else
|
|
{
|
|
menu_entries_append(list,
|
|
"/storage/emulated/0",
|
|
msg_hash_to_str(MSG_REMOVABLE_STORAGE),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
listed_storage_path = "/storage/emulated/0";
|
|
}
|
|
}
|
|
|
|
if (!g_android->is_play_store_build)
|
|
menu_entries_append(list,
|
|
"/storage",
|
|
msg_hash_to_str(MSG_REMOVABLE_STORAGE),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
if (*internal_storage_app_path)
|
|
{
|
|
if (g_android->is_play_store_build)
|
|
{
|
|
char user_data_app_path[PATH_MAX_LENGTH];
|
|
fill_pathname_join_special(user_data_app_path,
|
|
internal_storage_app_path, "RetroArch",
|
|
sizeof(user_data_app_path));
|
|
|
|
menu_entries_append(list,
|
|
user_data_app_path,
|
|
msg_hash_to_str(MSG_EXTERNAL_APPLICATION_DIR),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
|
|
menu_entries_append(list,
|
|
internal_storage_app_path,
|
|
msg_hash_to_str(MSG_EXTERNAL_APPLICATION_DIR),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
if (*app_dir)
|
|
menu_entries_append(list,
|
|
app_dir,
|
|
msg_hash_to_str(MSG_APPLICATION_DIR),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
for (unsigned i=0; i < volume_count; i++)
|
|
{
|
|
static char aux_path[PATH_MAX_LENGTH];
|
|
char index[2];
|
|
index[0] = '\0';
|
|
|
|
snprintf(index, sizeof(index), "%d", i);
|
|
|
|
CALL_OBJ_METHOD_PARAM(env, jstr, g_android->activity->clazz, g_android->getVolumePath,
|
|
(*env)->NewStringUTF(env, index));
|
|
|
|
if (jstr)
|
|
{
|
|
const char *str = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
|
|
aux_path[0] = '\0';
|
|
|
|
if (str && *str)
|
|
strlcpy(aux_path, str,
|
|
sizeof(aux_path));
|
|
|
|
(*env)->ReleaseStringUTFChars(env, jstr, str);
|
|
/* The primary volume is the shared-storage entry appended
|
|
* above; listing it again only duplicates the path. */
|
|
if (*aux_path && !string_is_equal(aux_path, listed_storage_path))
|
|
menu_entries_append(list,
|
|
aux_path,
|
|
msg_hash_to_str(MSG_APPLICATION_DIR),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
}
|
|
|
|
#elif defined(WEBOS)
|
|
if (path_is_directory("/media/developer/temp"))
|
|
menu_entries_append(list, "/media/developer/temp",
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
|
|
if (path_is_directory("/media/internal"))
|
|
menu_entries_append(list, "/media/internal",
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
|
|
if (path_is_directory("/tmp/usb"))
|
|
menu_entries_append(list, "/tmp/usb",
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
#else
|
|
char base_path[PATH_MAX] = {0};
|
|
char udisks_media_path[PATH_MAX] = {0};
|
|
const char *home = getenv("HOME");
|
|
const char *user = getenv("USER");
|
|
|
|
#if defined(DINGUX)
|
|
dingux_get_base_path(base_path, sizeof(base_path));
|
|
#else
|
|
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
|
|
if (xdg)
|
|
{
|
|
size_t _len = strlcpy(base_path, xdg, sizeof(base_path));
|
|
strlcpy_lit(base_path + _len, "/retroarch", sizeof(base_path) - _len);
|
|
}
|
|
else if (home)
|
|
{
|
|
size_t _len = strlcpy(base_path, home, sizeof(base_path));
|
|
strlcpy_lit(base_path + _len, "/.config/retroarch", sizeof(base_path) - _len);
|
|
}
|
|
#endif
|
|
|
|
{
|
|
size_t _len = strlcpy_lit(udisks_media_path, "/run/media", sizeof(udisks_media_path));
|
|
if (user)
|
|
{
|
|
_len += strlcpy_lit(udisks_media_path + _len, "/", sizeof(udisks_media_path) - _len);
|
|
strlcpy(udisks_media_path + _len, user, sizeof(udisks_media_path) - _len);
|
|
}
|
|
}
|
|
|
|
if (*base_path)
|
|
{
|
|
menu_entries_append(list, base_path,
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
if (home && *home)
|
|
{
|
|
menu_entries_append(list, home,
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
if (path_is_directory(udisks_media_path))
|
|
{
|
|
menu_entries_append(list, udisks_media_path,
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
if (path_is_directory("/media"))
|
|
{
|
|
menu_entries_append(list, "/media",
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
if (path_is_directory("/mnt"))
|
|
{
|
|
menu_entries_append(list, "/mnt",
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
#endif
|
|
|
|
#ifdef ANDROID
|
|
if (!g_android->is_play_store_build)
|
|
#else
|
|
if (1)
|
|
#endif
|
|
{
|
|
menu_entries_append(list, "/",
|
|
MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR_STR,
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
}
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (g_android->have_saf)
|
|
{
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (env != NULL)
|
|
{
|
|
jarray trees = (*env)->CallObjectMethod(env, g_android->activity->clazz, g_android->getPersistedSafTrees);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
else
|
|
{
|
|
jsize trees_length = (*env)->GetArrayLength(env, trees);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
else
|
|
{
|
|
jsize i;
|
|
for (i = 0; i < trees_length; ++i)
|
|
{
|
|
const char *tree_chars;
|
|
char *serialized_path;
|
|
jstring tree = (*env)->GetObjectArrayElement(env, trees, i);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
continue;
|
|
}
|
|
tree_chars = (*env)->GetStringUTFChars(env, tree, NULL);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
(*env)->DeleteLocalRef(env, tree);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
continue;
|
|
}
|
|
if ((serialized_path = retro_vfs_path_join_saf(tree_chars, "")) != NULL)
|
|
{
|
|
menu_entries_append(list,
|
|
serialized_path,
|
|
msg_hash_to_str(MSG_REMOVABLE_STORAGE),
|
|
enum_idx,
|
|
FILE_TYPE_DIRECTORY, 0, 0, NULL);
|
|
free(serialized_path);
|
|
}
|
|
(*env)->ReleaseStringUTFChars(env, tree, tree_chars);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
(*env)->DeleteLocalRef(env, tree);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
}
|
|
}
|
|
(*env)->DeleteLocalRef(env, trees);
|
|
if ((*env)->ExceptionOccurred(env))
|
|
{
|
|
(*env)->ExceptionDescribe(env);
|
|
(*env)->ExceptionClear(env);
|
|
}
|
|
}
|
|
}
|
|
|
|
menu_entries_append(list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FILE_BROWSER_OPEN_PICKER),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER),
|
|
MENU_ENUM_LABEL_FILE_BROWSER_OPEN_PICKER,
|
|
MENU_SETTING_ACTION, 0, 0, NULL);
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifndef ANDROID
|
|
|
|
static bool frontend_unix_set_fork(enum frontend_fork fork_mode)
|
|
{
|
|
switch (fork_mode)
|
|
{
|
|
case FRONTEND_FORK_CORE:
|
|
unix_fork_mode = fork_mode;
|
|
break;
|
|
case FRONTEND_FORK_CORE_WITH_ARGS:
|
|
unix_fork_mode = fork_mode;
|
|
break;
|
|
case FRONTEND_FORK_RESTART:
|
|
unix_fork_mode = FRONTEND_FORK_CORE;
|
|
|
|
{
|
|
char executable_path[PATH_MAX_LENGTH] = {0};
|
|
fill_pathname_application_path(executable_path,
|
|
sizeof(executable_path));
|
|
path_set(RARCH_PATH_CORE, executable_path);
|
|
}
|
|
command_event(CMD_EVENT_QUIT, NULL);
|
|
break;
|
|
case FRONTEND_FORK_NONE:
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void frontend_unix_exec(const char *path, bool should_load_content)
|
|
{
|
|
char *newargv[] = { NULL, NULL };
|
|
size_t _len = strlen(path) + 1;
|
|
|
|
newargv[0] = (char*)malloc(_len);
|
|
|
|
/* NULL-check malloc: the strlcpy on the next line
|
|
* NULL-derefs on OOM. Void function called from within
|
|
* an exec/fork flow; logging and returning leaves the
|
|
* caller able to retry or surface an error. Prior to
|
|
* this patch _len was strlen(path) (not +1), which meant
|
|
* strlcpy silently truncated the last character of the
|
|
* path because strlcpy needs n bytes to write n-1 chars
|
|
* plus a NUL. */
|
|
if (!newargv[0])
|
|
{
|
|
RARCH_ERR("Failed to allocate argv for exec.\n");
|
|
return;
|
|
}
|
|
|
|
strlcpy(newargv[0], path, _len);
|
|
|
|
execv(path, newargv);
|
|
}
|
|
|
|
static void frontend_unix_exitspawn(char *s, size_t len, char *args)
|
|
{
|
|
bool should_load_content = false;
|
|
|
|
if (unix_fork_mode == FRONTEND_FORK_NONE)
|
|
return;
|
|
|
|
switch (unix_fork_mode)
|
|
{
|
|
case FRONTEND_FORK_CORE_WITH_ARGS:
|
|
should_load_content = true;
|
|
break;
|
|
case FRONTEND_FORK_NONE:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
frontend_unix_exec(s, should_load_content);
|
|
}
|
|
#endif
|
|
|
|
/*#include <valgrind/valgrind.h>*/
|
|
static void frontend_unix_sighandler(int sig)
|
|
{
|
|
#ifdef VALGRIND_PRINTF_BACKTRACE
|
|
VALGRIND_PRINTF_BACKTRACE("SIGINT");
|
|
#endif
|
|
(void)sig;
|
|
unix_sighandler_quit++;
|
|
if (unix_sighandler_quit == 1)
|
|
{
|
|
#if defined(HAVE_SDL_DINGUX)
|
|
retroarch_ctl(RARCH_CTL_SET_SHUTDOWN, NULL);
|
|
#endif
|
|
}
|
|
if (unix_sighandler_quit == 2) exit(1);
|
|
/* in case there's a second deadlock in a C++ destructor or something */
|
|
if (unix_sighandler_quit >= 3) abort();
|
|
}
|
|
|
|
static void frontend_unix_install_signal_handlers(void)
|
|
{
|
|
struct sigaction sa;
|
|
|
|
sa.sa_sigaction = NULL;
|
|
sa.sa_handler = frontend_unix_sighandler;
|
|
sa.sa_flags = SA_RESTART;
|
|
sigemptyset(&sa.sa_mask);
|
|
sigaction(SIGINT, &sa, NULL);
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
}
|
|
|
|
static int frontend_unix_get_signal_handler_state(void)
|
|
{
|
|
return (int)unix_sighandler_quit;
|
|
}
|
|
|
|
static void frontend_unix_set_signal_handler_state(int value)
|
|
{
|
|
unix_sighandler_quit = value;
|
|
}
|
|
|
|
static void frontend_unix_destroy_signal_handler_state(void)
|
|
{
|
|
unix_sighandler_quit = 0;
|
|
}
|
|
|
|
/* To free change_data, call the function again with a NULL
|
|
* string_list while providing change_data again */
|
|
void android_app_set_window_settings(bool notch_write_over,
|
|
bool auto_mouse_grab)
|
|
{
|
|
#ifdef ANDROID
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (!env || !g_android)
|
|
return;
|
|
|
|
if (g_android->setWindowSettings)
|
|
CALL_VOID_METHOD_PARAM(env, g_android->activity->clazz,
|
|
g_android->setWindowSettings,
|
|
(jboolean)notch_write_over, (jboolean)auto_mouse_grab);
|
|
#endif
|
|
}
|
|
|
|
static void frontend_unix_set_sustained_performance_mode(bool on)
|
|
{
|
|
#ifdef ANDROID
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (!env || !g_android)
|
|
return;
|
|
|
|
if (g_android->setSustainedPerformanceMode)
|
|
CALL_VOID_METHOD_PARAM(env, g_android->activity->clazz,
|
|
g_android->setSustainedPerformanceMode, on);
|
|
#endif
|
|
}
|
|
|
|
static const char* frontend_unix_get_cpu_model_name(void)
|
|
{
|
|
#ifdef ANDROID
|
|
return NULL;
|
|
#else
|
|
cpu_features_get_model_name(unix_cpu_model_name,
|
|
sizeof(unix_cpu_model_name));
|
|
return unix_cpu_model_name;
|
|
#endif
|
|
}
|
|
|
|
enum retro_language frontend_unix_get_user_language(void)
|
|
{
|
|
enum retro_language lang = RETRO_LANGUAGE_ENGLISH;
|
|
#ifdef HAVE_LANGEXTRA
|
|
#ifdef ANDROID
|
|
jstring jstr = NULL;
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (!env || !g_android)
|
|
return lang;
|
|
|
|
if (g_android->getUserLanguageString)
|
|
{
|
|
CALL_OBJ_METHOD(env, jstr,
|
|
g_android->activity->clazz, g_android->getUserLanguageString);
|
|
|
|
if (jstr)
|
|
{
|
|
const char *lang_str = (*env)->GetStringUTFChars(env, jstr, 0);
|
|
lang = retroarch_get_language_from_iso(lang_str);
|
|
|
|
(*env)->ReleaseStringUTFChars(env, jstr, lang_str);
|
|
}
|
|
}
|
|
#else
|
|
char *envvar = getenv("LANG");
|
|
if (envvar)
|
|
return retroarch_get_language_from_iso(envvar);
|
|
#endif
|
|
#endif
|
|
return lang;
|
|
}
|
|
|
|
#if (defined(__linux__) || defined(__HAIKU__) || defined(__unix__)) && !defined(ANDROID)
|
|
static bool is_narrator_running_unix(void)
|
|
{
|
|
return (kill(speak_pid, 0) == 0);
|
|
}
|
|
|
|
static const char* accessibility_unix_language_code(const char* language)
|
|
{
|
|
if (
|
|
string_is_equal(language, "en") ||
|
|
string_is_equal(language, "it") ||
|
|
string_is_equal(language, "sv") ||
|
|
string_is_equal(language, "fr") ||
|
|
string_is_equal(language, "de") ||
|
|
string_is_equal(language, "he") ||
|
|
string_is_equal(language, "id") ||
|
|
string_is_equal(language, "es") ||
|
|
string_is_equal(language, "nl") ||
|
|
string_is_equal(language, "ro") ||
|
|
string_is_equal(language, "th") ||
|
|
string_is_equal(language, "ja") ||
|
|
string_is_equal(language, "sk") ||
|
|
string_is_equal(language, "hi") ||
|
|
string_is_equal(language, "ar") ||
|
|
string_is_equal(language, "hu") ||
|
|
string_is_equal(language, "el") ||
|
|
string_is_equal(language, "ru") ||
|
|
string_is_equal(language, "nb") ||
|
|
string_is_equal(language, "da") ||
|
|
string_is_equal(language, "fi") ||
|
|
string_is_equal(language, "tr") ||
|
|
string_is_equal(language, "ko") ||
|
|
string_is_equal(language, "pl") ||
|
|
string_is_equal(language, "cs") ||
|
|
string_is_equal(language, "eo") ||
|
|
string_is_equal(language, "vi") ||
|
|
string_is_equal(language, "fa") ||
|
|
string_is_equal(language, "uk") ||
|
|
string_is_equal(language, "be") ||
|
|
string_is_equal(language, "hr") ||
|
|
string_is_equal(language, "bg") ||
|
|
string_is_equal(language, "bn") ||
|
|
string_is_equal(language, "eu") ||
|
|
string_is_equal(language, "az") ||
|
|
string_is_equal(language, "sq") ||
|
|
string_is_equal(language, "af") ||
|
|
string_is_equal(language, "et") ||
|
|
string_is_equal(language, "ka") ||
|
|
string_is_equal(language, "gu") ||
|
|
string_is_equal(language, "ht") ||
|
|
string_is_equal(language, "is") ||
|
|
string_is_equal(language, "ga") ||
|
|
string_is_equal(language, "kn") ||
|
|
string_is_equal(language, "la") ||
|
|
string_is_equal(language, "lv") ||
|
|
string_is_equal(language, "lt") ||
|
|
string_is_equal(language, "mk") ||
|
|
string_is_equal(language, "ms") ||
|
|
string_is_equal(language, "mt") ||
|
|
string_is_equal(language, "sr") ||
|
|
string_is_equal(language, "sl") ||
|
|
string_is_equal(language, "sw") ||
|
|
string_is_equal(language, "ta") ||
|
|
string_is_equal(language, "te") ||
|
|
string_is_equal(language, "ur") ||
|
|
string_is_equal(language, "cy")
|
|
)
|
|
return language;
|
|
else if (
|
|
string_is_equal(language, "no") ||
|
|
string_is_equal(language, "nb")
|
|
)
|
|
return "nb";
|
|
else if (string_is_equal(language, "en_gb"))
|
|
return "en-gb";
|
|
else if (
|
|
string_is_equal(language, "ca") ||
|
|
string_is_equal(language, "ca_ES@valencia")
|
|
)
|
|
return "ca";
|
|
else if (
|
|
string_is_equal(language, "pt_pt") ||
|
|
string_is_equal(language, "pt")
|
|
)
|
|
return "pt";
|
|
else if (string_is_equal(language, "pt_bt"))
|
|
return "pt-br";
|
|
else if (
|
|
string_is_equal(language, "zh") ||
|
|
string_is_equal(language, "zh_cn") ||
|
|
string_is_equal(language, "zh_tw") ||
|
|
string_is_equal(language, "zh-CN") ||
|
|
string_is_equal(language, "zh-TW")
|
|
)
|
|
return "cmn";
|
|
else if (string_is_equal(language, "zh_hk"))
|
|
return "yue";
|
|
/* default voice as fallback */
|
|
return "en";
|
|
}
|
|
|
|
static bool accessibility_speak_unix(int speed,
|
|
const char* speak_text, int priority)
|
|
{
|
|
int pid;
|
|
settings_t *settings = config_get_ptr();
|
|
unsigned engine = settings
|
|
? settings->uints.accessibility_narrator_engine
|
|
: (unsigned)ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK;
|
|
const char* language = accessibility_unix_language_code(get_user_language_iso639_1(true));
|
|
char* voice_out = (char*)malloc(3 + strlen(language));
|
|
char* speed_out = (char*)malloc(3 + 3);
|
|
const char* speeds[10] = {"80", "100", "125", "150", "170", "210", "260", "310", "380", "450"};
|
|
/* speech-dispatcher (spd-say) takes a rate in the range -100..100,
|
|
* mapped here from RetroArch's 1..10 speed ladder. */
|
|
const char* spd_rates[10] = {"-90", "-70", "-50", "-30", "-10", "10", "30", "50", "70", "90"};
|
|
|
|
/* NULL-check both mallocs: voice_out[0]='-' and speed_out[0]='-'
|
|
* below NULL-deref on OOM. The 'end:' label does NULL-tolerant
|
|
* free()s on both pointers, so we can simply skip to it on
|
|
* either failure. Returning true matches the function's
|
|
* existing always-true return contract and means the
|
|
* accessibility request is silently dropped rather than
|
|
* surfacing a user-visible error for a non-critical feature. */
|
|
if (!voice_out || !speed_out)
|
|
goto end;
|
|
|
|
if (speed < 1)
|
|
speed = 1;
|
|
else if (speed > 10)
|
|
speed = 10;
|
|
|
|
voice_out[0] = '-';
|
|
voice_out[1] = 'v';
|
|
voice_out[2] = '\0';
|
|
strlcat(voice_out, language, 3 + strlen(language));
|
|
|
|
speed_out[0] = '-';
|
|
speed_out[1] = 's';
|
|
speed_out[2] = '\0';
|
|
strlcat(speed_out, speeds[speed-1], 6);
|
|
|
|
if (priority < 10 && speak_pid > 0)
|
|
{
|
|
/* check if old pid is running */
|
|
if (is_narrator_running_unix())
|
|
goto end;
|
|
}
|
|
|
|
if (speak_pid > 0)
|
|
{
|
|
/* Kill the running narrator */
|
|
kill(speak_pid, SIGTERM);
|
|
speak_pid = 0;
|
|
}
|
|
|
|
pid = fork();
|
|
switch (pid)
|
|
{
|
|
case 0:
|
|
if (engine == ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER)
|
|
{
|
|
/* child process: speech-dispatcher via spd-say.
|
|
* -l <lang> language
|
|
* -r <rate> rate (-100..100)
|
|
* -w wait until finished, so this child stays
|
|
* alive for is_narrator_running_unix()/SIGTERM
|
|
* to track and interrupt, matching the espeak
|
|
* long-lived-child behaviour. */
|
|
char* cmd[] = { (char*) "spd-say",
|
|
(char*) "-l", NULL,
|
|
(char*) "-r", NULL,
|
|
(char*) "-w",
|
|
NULL, NULL };
|
|
cmd[2] = (char*)language;
|
|
cmd[4] = (char*)spd_rates[speed-1];
|
|
cmd[6] = (char*)speak_text;
|
|
execvp("spd-say", cmd);
|
|
|
|
RARCH_WARN("Could not execute spd-say.\n");
|
|
/* Prevent interfere with the parent process */
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
else
|
|
{
|
|
/* child process: replace process with the espeak command */
|
|
char* cmd[] = { (char*) "espeak", NULL, NULL, NULL, NULL };
|
|
cmd[1] = voice_out;
|
|
cmd[2] = speed_out;
|
|
cmd[3] = (char*)speak_text;
|
|
execvp("espeak", cmd);
|
|
|
|
RARCH_WARN("Could not execute espeak.\n");
|
|
/* Prevent interfere with the parent process */
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
case -1:
|
|
RARCH_ERR("Could not fork for narrator.\n");
|
|
default:
|
|
{
|
|
/* parent process */
|
|
speak_pid = pid;
|
|
|
|
/* Tell the system that we'll ignore the exit status of the child
|
|
* process. This prevents zombie processes. */
|
|
signal(SIGCHLD, SIG_IGN);
|
|
}
|
|
}
|
|
|
|
end:
|
|
if (voice_out)
|
|
free(voice_out);
|
|
if (speed_out)
|
|
free(speed_out);
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
#ifdef ANDROID
|
|
bool is_screen_reader_enabled(void)
|
|
{
|
|
JNIEnv *env = jni_thread_getenv();
|
|
jboolean jbool = JNI_FALSE;
|
|
|
|
if (env != NULL)
|
|
CALL_BOOLEAN_METHOD(env, jbool,
|
|
g_android->activity->clazz, g_android->isScreenReaderEnabled);
|
|
|
|
return jbool == JNI_TRUE;
|
|
}
|
|
|
|
static bool is_narrator_running_android(void)
|
|
{
|
|
/* Screen reader is speaking on Android is controlled by the operating
|
|
* system, so return false to align with the rest of the API. */
|
|
return false;
|
|
}
|
|
|
|
static bool accessibility_speak_android(int speed,
|
|
const char* speak_text, int priority)
|
|
{
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (env != NULL)
|
|
CALL_VOID_METHOD_PARAM(env, g_android->activity->clazz,
|
|
g_android->accessibilitySpeak,
|
|
(*env)->NewStringUTF(env, speak_text));
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
static enum rarch_display_type frontend_unix_get_display_type(void)
|
|
{
|
|
#if defined(ANDROID)
|
|
return RARCH_DISPLAY_NONE;
|
|
#elif defined(HAVE_WAYLAND)
|
|
if (getenv("WAYLAND_DISPLAY"))
|
|
return RARCH_DISPLAY_WAYLAND;
|
|
#endif
|
|
#if defined(HAVE_X11)
|
|
if (getenv("DISPLAY"))
|
|
return RARCH_DISPLAY_X11;
|
|
#endif
|
|
return RARCH_DISPLAY_NONE;
|
|
}
|
|
|
|
frontend_ctx_driver_t frontend_ctx_unix = {
|
|
frontend_unix_get_env, /* get_env */
|
|
frontend_unix_init, /* init */
|
|
frontend_unix_deinit, /* deinit */
|
|
#ifdef ANDROID
|
|
NULL, /* exitspawn */
|
|
#else
|
|
frontend_unix_exitspawn, /* exitspawn */
|
|
#endif
|
|
NULL, /* process_args */
|
|
#ifdef ANDROID
|
|
NULL, /* exec */
|
|
NULL, /* set_fork */
|
|
#else
|
|
frontend_unix_exec, /* exec */
|
|
frontend_unix_set_fork, /* set_fork */
|
|
#endif
|
|
#ifdef ANDROID
|
|
frontend_android_shutdown, /* shutdown */
|
|
frontend_android_get_name, /* get_name */
|
|
#else
|
|
NULL, /* shutdown */
|
|
NULL, /* get_name */
|
|
#endif
|
|
frontend_unix_get_os,
|
|
NULL, /* content_loaded */
|
|
frontend_unix_get_arch, /* get_architecture */
|
|
frontend_unix_get_powerstate,
|
|
frontend_unix_parse_drive_list,
|
|
frontend_unix_install_signal_handlers,
|
|
frontend_unix_get_signal_handler_state,
|
|
frontend_unix_set_signal_handler_state,
|
|
frontend_unix_destroy_signal_handler_state,
|
|
NULL, /* attach_console */
|
|
NULL, /* detach_console */
|
|
#ifdef HAVE_LAKKA
|
|
frontend_unix_get_lakka_version, /* get_lakka_version */
|
|
#else
|
|
NULL, /* get_lakka_version */
|
|
#endif
|
|
#if defined(HAVE_LAKKA_SWITCH) || (defined(HAVE_LAKKA) && defined(HAVE_ODROIDGO2))
|
|
frontend_unix_set_screen_brightness,/* set_screen_brightness */
|
|
#else
|
|
NULL, /* set_screen_brightness */
|
|
#endif
|
|
frontend_unix_set_sustained_performance_mode,
|
|
frontend_unix_get_cpu_model_name,
|
|
frontend_unix_get_user_language,
|
|
#if (defined(__linux__) || defined(__HAIKU__) || defined(__unix__)) && !defined(ANDROID)
|
|
is_narrator_running_unix, /* is_narrator_running */
|
|
accessibility_speak_unix, /* accessibility_speak */
|
|
#else
|
|
is_narrator_running_android, /* is_narrator_running */
|
|
accessibility_speak_android, /* accessibility_speak */
|
|
#endif
|
|
#ifdef FERAL_GAMEMODE
|
|
frontend_unix_set_gamemode,
|
|
#else
|
|
NULL,
|
|
#endif
|
|
frontend_unix_get_display_type,
|
|
#ifdef ANDROID
|
|
"android", /* ident */
|
|
#else
|
|
"unix", /* ident */
|
|
#endif
|
|
NULL /* get_video_driver */
|
|
};
|