mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
The Dingux helpers live under os/dingux with the other platforms' system code.
4933 lines
162 KiB
C
4933 lines
162 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>
|
|
#include <sys/wait.h>
|
|
|
|
#ifdef __linux__
|
|
#include <linux/version.h>
|
|
#endif
|
|
|
|
#include <signal.h>
|
|
#include <rthreads/rthreads.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "../../config.h"
|
|
#endif
|
|
|
|
/* Builds without config.h keep Game Mode; configure builds follow
|
|
* --enable/--disable-gamemode. */
|
|
#include "gamemode_linux.h"
|
|
#if defined(RARCH_HAVE_GAMEMODE_LINUX) \
|
|
&& (!defined(HAVE_CONFIG_H) || defined(HAVE_GAMEMODE))
|
|
#define UNIX_GAMEMODE
|
|
#endif
|
|
|
|
#ifdef ANDROID
|
|
#include <android/log.h>
|
|
#include <android/native_window_jni.h>
|
|
#include <sys/system_properties.h>
|
|
#include <lists/string_list.h>
|
|
#include "../../audio/common/android_audio.h"
|
|
#ifdef HAVE_AAUDIO
|
|
#include "../../audio/audio_driver.h"
|
|
#endif
|
|
#ifdef HAVE_SAF
|
|
#include <vfs/vfs_implementation_saf.h>
|
|
#include "../../menu/menu_cbs.h"
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(DINGUX)
|
|
#include "../../os/dingux/dingux_utils.h"
|
|
#endif
|
|
|
|
#include <boolean.h>
|
|
#include <retro_atomic.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 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)
|
|
/* The narrator is a direct child, reaped with waitpid(WNOHANG): SIGCHLD
|
|
* keeps its default action, so every other child stays waitable. */
|
|
static pid_t speak_pid = 0;
|
|
/* Narrators sent SIGTERM that had not exited yet: reaped on later calls. */
|
|
#define NARRATOR_STOPPING_MAX 4
|
|
static pid_t speak_stopping[NARRATOR_STOPPING_MAX];
|
|
#endif
|
|
|
|
/* Counts SIGINT/SIGTERM. Written by the signal handler and read by
|
|
* the main thread and, through x11_alive(), the threaded video worker:
|
|
* an atomic rather than a volatile sig_atomic_t, which is only safe
|
|
* between a handler and the thread it interrupted. Lock-free for int,
|
|
* so usable in the handler. */
|
|
static retro_atomic_int_t unix_sighandler_quit = RETRO_ATOMIC_INT_INITIALIZER(0);
|
|
|
|
#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_arg(struct android_app *android_app,
|
|
int8_t cmd, void *arg)
|
|
{
|
|
struct android_app_msg msg;
|
|
ssize_t ret;
|
|
|
|
if (!android_app)
|
|
return false;
|
|
|
|
memset(&msg, 0, sizeof(msg));
|
|
msg.cmd = cmd;
|
|
msg.arg = arg;
|
|
|
|
/* ART suspends threads with a signal, and the handler is not
|
|
* guaranteed to carry SA_RESTART, so a write can come back short.
|
|
* It cannot come back partial: PIPE_BUF-sized writes are atomic. */
|
|
do
|
|
{
|
|
ret = write(android_app->msgwrite, &msg, sizeof(msg));
|
|
} while (ret < 0 && errno == EINTR);
|
|
|
|
if (ret == (ssize_t)sizeof(msg))
|
|
return true;
|
|
|
|
RARCH_ERR("[Android] Failed to queue app command %d.\n", (int)cmd);
|
|
return false;
|
|
}
|
|
|
|
bool android_app_write_cmd(struct android_app *android_app, int8_t cmd)
|
|
{
|
|
return android_app_write_cmd_arg(android_app, cmd, NULL);
|
|
}
|
|
|
|
static bool android_app_thread_gone(struct android_app *android_app)
|
|
{
|
|
return (android_lifecycle_flags(&android_app->lc)
|
|
& ANDROID_LC_EXITED) != 0;
|
|
}
|
|
|
|
static void android_app_set_input(struct android_app *android_app,
|
|
AInputQueue* inputQueue)
|
|
{
|
|
unsigned ticket;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
ticket = android_lifecycle_last_ticket(&android_app->lc);
|
|
if ( !android_app_thread_gone(android_app)
|
|
&& android_app_write_cmd_arg(android_app, APP_CMD_INPUT_CHANGED,
|
|
inputQueue))
|
|
ticket = android_lifecycle_ticket(&android_app->lc);
|
|
|
|
android_lifecycle_wait(&android_app->lc, ANDROID_LC_UNTIL_DONE,
|
|
(int)ticket, true, -1);
|
|
}
|
|
|
|
/* 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 last one answered and the wait falls through. */
|
|
static void android_app_set_window(struct android_app *android_app,
|
|
ANativeWindow* window)
|
|
{
|
|
unsigned ticket;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
ticket = android_lifecycle_last_ticket(&android_app->lc);
|
|
|
|
if ( !android_app_thread_gone(android_app)
|
|
&& android_app->posted_window
|
|
&& android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW))
|
|
ticket = android_lifecycle_ticket(&android_app->lc);
|
|
|
|
android_app->posted_window = window;
|
|
|
|
if ( !android_app_thread_gone(android_app)
|
|
&& window
|
|
&& android_app_write_cmd_arg(android_app, APP_CMD_INIT_WINDOW,
|
|
window))
|
|
ticket = android_lifecycle_ticket(&android_app->lc);
|
|
|
|
android_lifecycle_wait(&android_app->lc, ANDROID_LC_UNTIL_DONE,
|
|
(int)ticket, true, -1);
|
|
}
|
|
|
|
void android_second_surface_free(struct android_second_surface *node)
|
|
{
|
|
ANativeWindow_release(node->window);
|
|
free(node);
|
|
}
|
|
|
|
void android_second_display_request(unsigned ordinal, bool given_up)
|
|
{
|
|
JNIEnv *env;
|
|
unsigned number;
|
|
struct android_app *android_app = (struct android_app*)g_android;
|
|
struct android_second_display *second;
|
|
|
|
if (!android_app || !android_app->setSecondDisplay)
|
|
return;
|
|
if (!(env = jni_thread_getenv()))
|
|
return;
|
|
second = &android_app->second_display;
|
|
number = (unsigned)retro_atomic_fetch_add_int(&second->requests, 1) + 1;
|
|
/* Before Java can act on the hide */
|
|
if (given_up && !ordinal)
|
|
retro_atomic_store_release_int(&second->released, (int)number);
|
|
CALL_VOID_METHOD_PARAM(env, android_app->activity->clazz,
|
|
android_app->setSecondDisplay, (jint)ordinal, (jint)number);
|
|
}
|
|
|
|
/* UI thread: the second display's window goes. Never waits for the
|
|
* renderer: its own references keep the window alive, a frame still
|
|
* drawing into it gets an error from the abandoned queue, and the
|
|
* next frame lets it go (android_windows_poll()). */
|
|
static void android_second_surface_term(struct android_app *android_app)
|
|
{
|
|
struct android_second_display *second = &android_app->second_display;
|
|
struct android_second_surface *node = (struct android_second_surface*)
|
|
retro_atomic_exchange_ptr(&second->surface, NULL);
|
|
|
|
if (node)
|
|
android_second_surface_free(node);
|
|
retro_atomic_store_release_int(&second->surface_gone,
|
|
retro_atomic_load_acquire_int(&second->surface_generation));
|
|
android_app->posted_second_window = NULL;
|
|
android_app->posted_second_dims = 0;
|
|
/* A failure the going surface caused is not held against the next */
|
|
if (!android_app_thread_gone(android_app))
|
|
android_app_write_cmd(android_app, APP_CMD_SECOND_WINDOW_CHANGED);
|
|
}
|
|
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeDisplaysChanged(
|
|
JNIEnv *env, jclass clazz, jintArray connections, jintArray flags,
|
|
jintArray widths, jintArray heights, jobjectArray names)
|
|
{
|
|
video_window_display_t list[VIDEO_WINDOW_DISPLAYS_MAX];
|
|
jint conn[VIDEO_WINDOW_DISPLAYS_MAX];
|
|
jint bits[VIDEO_WINDOW_DISPLAYS_MAX];
|
|
jint w[VIDEO_WINDOW_DISPLAYS_MAX];
|
|
jint h[VIDEO_WINDOW_DISPLAYS_MAX];
|
|
jsize i, count;
|
|
struct android_app *android_app = g_android_early;
|
|
|
|
if ( !android_app || !connections || !flags || !widths || !heights
|
|
|| !names)
|
|
return;
|
|
count = (*env)->GetArrayLength(env, connections);
|
|
if (count > VIDEO_WINDOW_DISPLAYS_MAX)
|
|
count = VIDEO_WINDOW_DISPLAYS_MAX;
|
|
if ( (*env)->GetArrayLength(env, flags) < count
|
|
|| (*env)->GetArrayLength(env, widths) < count
|
|
|| (*env)->GetArrayLength(env, heights) < count
|
|
|| (*env)->GetArrayLength(env, names) < count)
|
|
return;
|
|
(*env)->GetIntArrayRegion(env, connections, 0, count, conn);
|
|
(*env)->GetIntArrayRegion(env, flags, 0, count, bits);
|
|
(*env)->GetIntArrayRegion(env, widths, 0, count, w);
|
|
(*env)->GetIntArrayRegion(env, heights, 0, count, h);
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
video_window_display_t *d = &list[i];
|
|
jstring name = (jstring)
|
|
(*env)->GetObjectArrayElement(env, names, i);
|
|
memset(d, 0, sizeof(*d));
|
|
d->ordinal = (unsigned)i + 1;
|
|
d->connection = (conn[i] >= 0
|
|
&& conn[i] <= VIDEO_WINDOW_CONNECTION_TRANSITIVE)
|
|
? (unsigned)conn[i] : VIDEO_WINDOW_CONNECTION_UNKNOWN;
|
|
d->own = (bits[i] & ANDROID_DISPLAY_OWN) != 0;
|
|
d->valid = (bits[i] & ANDROID_DISPLAY_VALID) != 0;
|
|
d->on = (bits[i] & ANDROID_DISPLAY_ON) != 0;
|
|
d->is_private = (bits[i] & ANDROID_DISPLAY_PRIVATE) != 0;
|
|
d->physical = (bits[i] & ANDROID_DISPLAY_PHYSICAL) != 0;
|
|
d->dims = (w[i] > 0 && h[i] > 0)
|
|
? VIDEO_SCALE_PACK(w[i], h[i]) : 0;
|
|
if (name)
|
|
{
|
|
const char *utf = (*env)->GetStringUTFChars(env, name, NULL);
|
|
if (utf)
|
|
{
|
|
strlcpy(d->name, utf, sizeof(d->name));
|
|
(*env)->ReleaseStringUTFChars(env, name, utf);
|
|
}
|
|
else
|
|
JNI_EXCEPTION(env);
|
|
(*env)->DeleteLocalRef(env, name);
|
|
}
|
|
}
|
|
|
|
video_driver_publish_window_displays(list, (unsigned)count);
|
|
android_app_write_cmd(android_app, APP_CMD_SECOND_WINDOW_CHANGED);
|
|
}
|
|
|
|
JNIEXPORT jint JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeSurfaceChanged(
|
|
JNIEnv *env, jclass clazz, jobject surface, jint width, jint height,
|
|
jint request)
|
|
{
|
|
ANativeWindow *window;
|
|
struct android_second_surface *node;
|
|
struct android_second_surface *stale;
|
|
unsigned dims;
|
|
int generation;
|
|
struct android_app *android_app = g_android_early;
|
|
struct android_second_display *second;
|
|
|
|
if (!android_app || !surface || width <= 0 || height <= 0)
|
|
return 0;
|
|
if (!(window = ANativeWindow_fromSurface(env, surface)))
|
|
{
|
|
RARCH_ERR("[Android] Second display: its surface has no window.\n");
|
|
return 0;
|
|
}
|
|
second = &android_app->second_display;
|
|
dims = VIDEO_SCALE_PACK(width, height);
|
|
generation = retro_atomic_load_acquire_int(&second->surface_generation);
|
|
|
|
if (window == android_app->posted_second_window)
|
|
{
|
|
if (dims == android_app->posted_second_dims)
|
|
{
|
|
ANativeWindow_release(window);
|
|
return generation;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (android_app->posted_second_window)
|
|
android_second_surface_term(android_app);
|
|
if (++generation <= 0)
|
|
generation = 1;
|
|
retro_atomic_store_release_int(&second->surface_generation,
|
|
generation);
|
|
}
|
|
|
|
if (!(node = (struct android_second_surface*)malloc(sizeof(*node))))
|
|
{
|
|
ANativeWindow_release(window);
|
|
return 0;
|
|
}
|
|
node->window = window;
|
|
node->dims = dims;
|
|
node->generation = generation;
|
|
node->request = (int)request;
|
|
android_app->posted_second_window = window;
|
|
android_app->posted_second_dims = dims;
|
|
|
|
if ((stale = (struct android_second_surface*)retro_atomic_exchange_ptr(
|
|
&second->surface, node)))
|
|
android_second_surface_free(stale);
|
|
android_app_write_cmd(android_app, APP_CMD_SECOND_WINDOW_CHANGED);
|
|
return generation;
|
|
}
|
|
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeSurfaceDestroyed(
|
|
JNIEnv *env, jclass clazz)
|
|
{
|
|
struct android_app *android_app = g_android_early;
|
|
|
|
if (android_app && android_app->posted_second_window)
|
|
android_second_surface_term(android_app);
|
|
}
|
|
|
|
/* UI thread: an app thread asleep in its looper while paused reads the
|
|
* second display's rings only once woken */
|
|
static void android_second_wake(struct android_app *android_app)
|
|
{
|
|
ALooper *looper = (ALooper*)retro_atomic_load_acquire_ptr(
|
|
&android_app->looper_to_wake);
|
|
if (looper)
|
|
ALooper_wake(looper);
|
|
}
|
|
|
|
/* For each pointer of a touch on the second display. Downs and moves
|
|
* keep room in the ring for every finger's up. */
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeTouch(
|
|
JNIEnv *env, jclass clazz, jint pointer_id, jint x, jint y,
|
|
jboolean down, jint generation)
|
|
{
|
|
/* Ids whose last record written was a down; UI thread only */
|
|
static uint32_t down_ids = 0;
|
|
android_second_touch_t touch;
|
|
uint32_t bit;
|
|
size_t room;
|
|
struct android_app *android_app = g_android_early;
|
|
|
|
if (!android_app)
|
|
return;
|
|
#ifdef DEBUG_ANDROID_INPUT
|
|
RARCH_LOG("[RA Input] Second display touch id=%d x=%d y=%d down=%d"
|
|
" surface=%d\n", (int)pointer_id, (int)x, (int)y,
|
|
(int)(down != JNI_FALSE), (int)generation);
|
|
#endif
|
|
/* MotionEvent ids are 0-31 */
|
|
if (pointer_id < 0 || pointer_id > 31)
|
|
return;
|
|
bit = (uint32_t)1 << pointer_id;
|
|
room = retro_spsc_write_avail(&android_app->second_display.touches);
|
|
if (down != JNI_FALSE)
|
|
{
|
|
if (room < (MAX_TOUCH + 1) * sizeof(touch))
|
|
return;
|
|
down_ids |= bit;
|
|
}
|
|
else
|
|
{
|
|
if (!(down_ids & bit) || room < sizeof(touch))
|
|
return;
|
|
down_ids &= ~bit;
|
|
}
|
|
touch.pointer_id = pointer_id;
|
|
touch.x = x;
|
|
touch.y = y;
|
|
touch.generation = generation;
|
|
touch.down = (down != JNI_FALSE);
|
|
retro_spsc_write(&android_app->second_display.touches, &touch,
|
|
sizeof(touch));
|
|
android_second_wake(android_app);
|
|
}
|
|
|
|
/* A down or a move keeps room in the ring for ups */
|
|
static void android_second_input_write(struct android_app *android_app,
|
|
const android_input_fields_t *in, bool up)
|
|
{
|
|
retro_spsc_t *ring = &android_app->second_display.inputs;
|
|
size_t need = up ? sizeof(*in)
|
|
: (ANDROID_SECOND_INPUT_UPS + 1) * sizeof(*in);
|
|
|
|
if (retro_spsc_write_avail(ring) < need)
|
|
return;
|
|
retro_spsc_write(ring, in, sizeof(*in));
|
|
android_second_wake(android_app);
|
|
}
|
|
|
|
/* A key the second display's window received; @event_time is
|
|
* KeyEvent.getEventTime(), milliseconds */
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeKey(
|
|
JNIEnv *env, jclass clazz, jint device_id, jint source, jint action,
|
|
jint keycode, jint flags, jint meta_state, jlong event_time)
|
|
{
|
|
android_input_fields_t key;
|
|
struct android_app *android_app = g_android_early;
|
|
|
|
if (!android_app)
|
|
return;
|
|
memset(&key, 0, sizeof(key));
|
|
key.event_time = (int64_t)event_time * 1000000;
|
|
key.type = AINPUT_EVENT_TYPE_KEY;
|
|
key.device_id = device_id;
|
|
key.source = source;
|
|
key.action = action;
|
|
key.keycode = keycode;
|
|
key.flags = flags;
|
|
key.meta_state = meta_state;
|
|
android_second_input_write(android_app, &key,
|
|
action == AKEY_EVENT_ACTION_UP);
|
|
}
|
|
|
|
/* A stick or hat move the second display's window received: the axes
|
|
* in ANDROID_JOYSTICK_AXES' order */
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeMotion(
|
|
JNIEnv *env, jclass clazz, jint device_id, jint source,
|
|
jfloatArray axes)
|
|
{
|
|
android_input_fields_t move;
|
|
struct android_app *android_app = g_android_early;
|
|
|
|
if ( !android_app || !axes
|
|
|| (*env)->GetArrayLength(env, axes) < ANDROID_JOYSTICK_AXES)
|
|
return;
|
|
memset(&move, 0, sizeof(move));
|
|
move.type = AINPUT_EVENT_TYPE_MOTION;
|
|
move.device_id = device_id;
|
|
move.source = source;
|
|
(*env)->GetFloatArrayRegion(env, axes, 0, ANDROID_JOYSTICK_AXES,
|
|
move.axes);
|
|
android_second_input_write(android_app, &move, false);
|
|
}
|
|
|
|
/* The second display's window gained or lost focus; Java also sends a
|
|
* loss when it dismisses it */
|
|
JNIEXPORT void JNICALL Java_com_retroarch_browser_retroactivity_SecondDisplayController_nativeFocusChanged(
|
|
JNIEnv *env, jclass clazz, jboolean focused)
|
|
{
|
|
struct android_app *android_app = g_android_early;
|
|
|
|
if (android_app)
|
|
android_app_write_cmd(android_app, focused != JNI_FALSE
|
|
? APP_CMD_SECOND_GAINED_FOCUS : APP_CMD_SECOND_LOST_FOCUS);
|
|
}
|
|
|
|
/* 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: the acknowledged state is
|
|
* the app thread's and nothing acts on it but the waiter, so giving up on the
|
|
* acknowledgement costs the caller nothing beyond returning before the app
|
|
* thread has caught up. PAUSE and STOP are acknowledged once SRAM, core
|
|
* options and the config have been written (see
|
|
* android_input_flush_pending_state()), so while the wait lasts the
|
|
* process cannot be killed with those unsaved.
|
|
*
|
|
* 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;
|
|
|
|
if (!android_app)
|
|
return;
|
|
|
|
android_app_write_cmd(android_app, cmd);
|
|
acked = android_lifecycle_wait(&android_app->lc, ANDROID_LC_UNTIL_STATE,
|
|
cmd, true, ANDROID_ACTIVITY_STATE_TIMEOUT_US);
|
|
|
|
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)
|
|
|
|
/* App thread that onDestroy() gave up waiting for. It is still running
|
|
* rarch_main() against the process-wide statics (task queue, drivers,
|
|
* runloop), so a second app thread must not start until it has left. */
|
|
static sthread_t *android_app_orphan_thread = NULL;
|
|
|
|
static void android_app_free(struct android_app* android_app)
|
|
{
|
|
bool acked;
|
|
|
|
/* onDestroy() hands this whatever android_app_create() returned, and
|
|
* that is NULL when the create failed. The other four callbacks
|
|
* that take the struct already tolerate it; this one dereferenced
|
|
* it. */
|
|
if (!android_app)
|
|
return;
|
|
|
|
/* Ask the thread to shut down, then wait for android_app_destroy()
|
|
* to say it has finished. destroy_from_framework reaches the app
|
|
* thread with the command: the pipe orders the two. */
|
|
android_app->destroy_from_framework = 1;
|
|
android_app_write_cmd(android_app, APP_CMD_DESTROY);
|
|
|
|
acked = android_lifecycle_wait(&android_app->lc, ANDROID_LC_UNTIL_FLAGS,
|
|
ANDROID_LC_DESTROYED, false, ANDROID_DESTROY_TIMEOUT_US);
|
|
|
|
/* 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");
|
|
android_app_orphan_thread = android_app->thread;
|
|
return;
|
|
}
|
|
|
|
sthread_join(android_app->thread);
|
|
|
|
{
|
|
ALooper *looper = (ALooper*)retro_atomic_exchange_ptr(
|
|
&android_app->looper_to_wake, NULL);
|
|
if (looper)
|
|
ALooper_release(looper);
|
|
}
|
|
|
|
close(android_app->msgread);
|
|
close(android_app->msgwrite);
|
|
android_lifecycle_free(&android_app->lc);
|
|
{
|
|
struct android_second_surface *node = (struct android_second_surface*)
|
|
retro_atomic_exchange_ptr(&android_app->second_display.surface,
|
|
NULL);
|
|
if (node)
|
|
android_second_surface_free(node);
|
|
}
|
|
retro_spsc_free(&android_app->second_display.touches);
|
|
retro_spsc_free(&android_app->second_display.inputs);
|
|
retro_spsc_free(&android_app->input_reader.events);
|
|
retro_eventcount_free(&android_app->input_reader.room);
|
|
|
|
/* A re-created activity's create may have replaced it already */
|
|
if (g_android_early == android_app)
|
|
g_android_early = NULL;
|
|
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;
|
|
|
|
/* The size before the flag, so a reader that observes @changed
|
|
* cannot still see the previous size and build a swapchain at the
|
|
* wrong resolution. */
|
|
retro_atomic_store_release_int(&instance->content_rect.dims,
|
|
(int)VIDEO_SCALE_PACK(width, height));
|
|
retro_atomic_store_release_int(&instance->content_rect.changed, 1);
|
|
}
|
|
|
|
/* Kept as the tree-wide entry point name; the caching, the attach and
|
|
* the detach-at-thread-exit all live in libretro-common's rjni now. */
|
|
JNIEnv *jni_thread_getenv(void)
|
|
{
|
|
return rjni_env();
|
|
}
|
|
|
|
static void android_app_entry(void *data)
|
|
{
|
|
struct android_app *android_app = (struct android_app*)data;
|
|
char arguments[] = "retroarch";
|
|
char *argv[] = {arguments, NULL};
|
|
int argc = 1;
|
|
|
|
sthread_setname("ra-main");
|
|
|
|
rarch_main(argc, argv, data);
|
|
|
|
/* Only two paths return here rather than exiting the process: an
|
|
* init failure inside rarch_main() before its own shutdown
|
|
* machinery runs, and the framework-destroy unwind (which
|
|
* android_app_free() is already waiting out). This thread is the
|
|
* sole consumer of the command pipe, so from here on no posted
|
|
* command can ever be acknowledged: retire every outstanding
|
|
* ticket by marking the consumer gone, or the next synchronous
|
|
* lifecycle callback - surfaceDestroyed() into
|
|
* android_app_set_window(NULL) - parks the Java UI thread on the
|
|
* wait until ActivityManager declares an ANR. The struct outlives
|
|
* this thread on every path: android_app_free() joins before
|
|
* freeing and deliberately leaks it when it orphans the thread
|
|
* instead. */
|
|
android_lifecycle_set_flags(&android_app->lc, ANDROID_LC_EXITED);
|
|
}
|
|
|
|
static struct android_app* android_app_create(ANativeActivity* activity,
|
|
void* savedState, size_t savedStateSize)
|
|
{
|
|
int msgpipe[2];
|
|
bool started;
|
|
struct android_app *android_app;
|
|
|
|
/* The activity can be recreated in the same process while the app
|
|
* thread of the previous one is still unwinding. Two rarch_main()
|
|
* instances share every static in the process, and the second one
|
|
* to run task_queue_deinit() joins a worker the first has already
|
|
* joined and freed, which bionic reports as an invalid pthread_t
|
|
* and aborts on. Nothing on the app thread ever waits for the UI
|
|
* thread, so the orphan always finishes on its own; waiting for it
|
|
* here is the only ordering under which the new instance starts
|
|
* from quiescent statics. */
|
|
if (android_app_orphan_thread)
|
|
{
|
|
RARCH_WARN("[Android] Waiting for the previous app thread "
|
|
"to exit before starting a new one.\n");
|
|
sthread_join(android_app_orphan_thread);
|
|
android_app_orphan_thread = NULL;
|
|
}
|
|
|
|
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;
|
|
|
|
if (!android_lifecycle_init(&android_app->lc))
|
|
{
|
|
free(android_app);
|
|
g_android_early = NULL;
|
|
RARCH_ERR("Failed to initialize android_app lifecycle state.\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);
|
|
android_lifecycle_free(&android_app->lc);
|
|
free(android_app);
|
|
g_android_early = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
android_app->msgread = msgpipe[0];
|
|
android_app->msgwrite = msgpipe[1];
|
|
|
|
/* A ring that cannot be made drops the second display's touches or
|
|
* keys; nothing else depends on them */
|
|
retro_spsc_init(&android_app->second_display.touches,
|
|
64 * sizeof(android_second_touch_t));
|
|
retro_spsc_init(&android_app->second_display.inputs,
|
|
ANDROID_SECOND_INPUTS * sizeof(android_input_fields_t));
|
|
|
|
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_LC_RUNNING, so the wait
|
|
* 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);
|
|
retro_spsc_free(&android_app->second_display.touches);
|
|
retro_spsc_free(&android_app->second_display.inputs);
|
|
android_lifecycle_free(&android_app->lc);
|
|
free(android_app);
|
|
g_android_early = NULL;
|
|
RARCH_ERR("Failed to spawn android_app thread.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Wait for the thread to start, or to leave without ever having
|
|
* started. RUNNING is set in frontend_unix_init(), a long way
|
|
* into rarch_main(); an init failure before that point returns from
|
|
* android_app_entry() with it still clear, so EXITED is the other
|
|
* way this wait ends.
|
|
*
|
|
* RUNNING is what decides the outcome: a thread that started and
|
|
* then exited quickly can set both before the wait is even
|
|
* entered, and that is a successful create. */
|
|
android_lifecycle_wait(&android_app->lc, ANDROID_LC_UNTIL_FLAGS,
|
|
ANDROID_LC_RUNNING | ANDROID_LC_EXITED, false, -1);
|
|
started = (android_lifecycle_flags(&android_app->lc)
|
|
& ANDROID_LC_RUNNING) != 0;
|
|
|
|
if (!started)
|
|
{
|
|
/* Nothing was initialised, so there is no teardown to
|
|
* orchestrate - and no reason to hand the framework an
|
|
* android_app it would keep delivering lifecycle callbacks to.
|
|
* The thread touches nothing after setting EXITED, so the join
|
|
* completes and the struct is ours to free. */
|
|
RARCH_ERR("[Android] App thread exited before it started.\n");
|
|
sthread_join(android_app->thread);
|
|
close(android_app->msgread);
|
|
close(android_app->msgwrite);
|
|
if (android_app->savedState)
|
|
free(android_app->savedState);
|
|
retro_spsc_free(&android_app->second_display.touches);
|
|
retro_spsc_free(&android_app->second_display.inputs);
|
|
android_lifecycle_free(&android_app->lc);
|
|
free(android_app);
|
|
g_android_early = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
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);
|
|
|
|
rjni_set_vm(activity->vm);
|
|
|
|
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 const char *android_audio_device_type(jint type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case 1: return "earpiece";
|
|
case 2: return "speaker";
|
|
case 3: return "wired headset";
|
|
case 4: return "wired headphones";
|
|
case 5: return "analog line";
|
|
case 6: return "digital line";
|
|
case 7: return "Bluetooth SCO";
|
|
case 8: return "Bluetooth A2DP";
|
|
case 9: return "HDMI";
|
|
case 10: return "HDMI ARC";
|
|
case 11: return "USB device";
|
|
case 12: return "USB accessory";
|
|
case 13: return "dock";
|
|
case 21: return "bus";
|
|
case 22: return "USB headset";
|
|
case 23: return "hearing aid";
|
|
case 26: return "BLE headset";
|
|
case 27: return "BLE speaker";
|
|
case 29: return "HDMI eARC";
|
|
case 30: return "BLE broadcast";
|
|
default: break;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* The largest entry of an int[] from Java; 0 when it is empty. With
|
|
* find, whether find is among them instead (1 or 0). */
|
|
static jint android_int_array_scan(JNIEnv *env, jobject arr, jint find,
|
|
bool want_find)
|
|
{
|
|
jsize n, k;
|
|
jint best = 0;
|
|
jint *v;
|
|
if (!arr)
|
|
return 0;
|
|
n = (*env)->GetArrayLength(env, (jintArray)arr);
|
|
if (n <= 0 || !(v = (*env)->GetIntArrayElements(env, (jintArray)arr, NULL)))
|
|
return 0;
|
|
for (k = 0; k < n; k++)
|
|
{
|
|
if (want_find && v[k] == find)
|
|
best = 1;
|
|
else if (!want_find && v[k] > best)
|
|
best = v[k];
|
|
}
|
|
(*env)->ReleaseIntArrayElements(env, (jintArray)arr, v, JNI_ABORT);
|
|
return best;
|
|
}
|
|
|
|
/* Walks AudioManager's outputs: appends each to list where one is
|
|
* given, and fills the caps of the output with want_id where that is
|
|
* not zero. True when that output was found. */
|
|
static bool android_audio_outputs_walk(struct string_list *list,
|
|
int32_t want_id, unsigned *max_channels, bool *ac3)
|
|
{
|
|
union string_list_elem_attr attr;
|
|
jclass ctx_class, am_class, dev_class = NULL;
|
|
jmethodID get_service, get_devices;
|
|
jmethodID get_id = NULL, get_type = NULL, get_name = NULL, to_string = NULL;
|
|
jmethodID get_counts = NULL, get_encodings = NULL;
|
|
jobject am, devices;
|
|
jstring svc;
|
|
jsize i, count;
|
|
bool found = false;
|
|
JNIEnv *env = jni_thread_getenv();
|
|
|
|
if (!env || !g_android || !g_android->activity)
|
|
return false;
|
|
if ((*env)->PushLocalFrame(env, 16) != 0)
|
|
{
|
|
JNI_EXCEPTION(env);
|
|
return false;
|
|
}
|
|
attr.i = 0;
|
|
|
|
svc = (*env)->NewStringUTF(env, "audio");
|
|
GET_OBJECT_CLASS(env, ctx_class, g_android->activity->clazz);
|
|
if (!svc || !ctx_class)
|
|
goto done;
|
|
GET_METHOD_ID(env, get_service, ctx_class, "getSystemService",
|
|
"(Ljava/lang/String;)Ljava/lang/Object;");
|
|
if (!get_service)
|
|
goto done;
|
|
CALL_OBJ_METHOD_PARAM(env, am, g_android->activity->clazz,
|
|
get_service, svc);
|
|
if (!am)
|
|
goto done;
|
|
GET_OBJECT_CLASS(env, am_class, am);
|
|
if (!am_class)
|
|
goto done;
|
|
GET_METHOD_ID(env, get_devices, am_class, "getDevices",
|
|
"(I)[Landroid/media/AudioDeviceInfo;");
|
|
if (!get_devices)
|
|
goto done;
|
|
/* AudioManager.GET_DEVICES_OUTPUTS */
|
|
CALL_OBJ_METHOD_PARAM(env, devices, am, get_devices, (jint)2);
|
|
if (!devices)
|
|
goto done;
|
|
|
|
count = (*env)->GetArrayLength(env, (jobjectArray)devices);
|
|
for (i = 0; i < count && !found; i++)
|
|
{
|
|
jobject dev;
|
|
jint id = 0;
|
|
jint type = 0;
|
|
|
|
if ((*env)->PushLocalFrame(env, 8) != 0)
|
|
{
|
|
JNI_EXCEPTION(env);
|
|
break;
|
|
}
|
|
dev = (*env)->GetObjectArrayElement(env, (jobjectArray)devices, i);
|
|
JNI_EXCEPTION(env);
|
|
if (dev && !dev_class)
|
|
{
|
|
jclass obj_class;
|
|
/* Popped with the frame below; the IDs outlive it. */
|
|
GET_OBJECT_CLASS(env, dev_class, dev);
|
|
FIND_CLASS(env, obj_class, "java/lang/Object");
|
|
if (dev_class)
|
|
{
|
|
GET_METHOD_ID(env, get_id, dev_class, "getId", "()I");
|
|
GET_METHOD_ID(env, get_type, dev_class, "getType", "()I");
|
|
GET_METHOD_ID(env, get_name, dev_class, "getProductName",
|
|
"()Ljava/lang/CharSequence;");
|
|
GET_METHOD_ID(env, get_counts, dev_class, "getChannelCounts", "()[I");
|
|
GET_METHOD_ID(env, get_encodings, dev_class, "getEncodings", "()[I");
|
|
}
|
|
if (obj_class)
|
|
GET_METHOD_ID(env, to_string, obj_class, "toString",
|
|
"()Ljava/lang/String;");
|
|
}
|
|
if (!dev || !get_id || !get_type)
|
|
{
|
|
(*env)->PopLocalFrame(env, NULL);
|
|
continue;
|
|
}
|
|
CALL_INT_METHOD(env, id, dev, get_id);
|
|
CALL_INT_METHOD(env, type, dev, get_type);
|
|
|
|
if (want_id && id == want_id)
|
|
{
|
|
jobject counts = NULL, encodings = NULL;
|
|
found = true;
|
|
if (get_counts)
|
|
CALL_OBJ_METHOD(env, counts, dev, get_counts);
|
|
if (get_encodings)
|
|
CALL_OBJ_METHOD(env, encodings, dev, get_encodings);
|
|
if (max_channels)
|
|
*max_channels = (unsigned)android_int_array_scan(env, counts, 0, false);
|
|
/* AudioFormat.ENCODING_AC3 */
|
|
if (ac3)
|
|
*ac3 = android_int_array_scan(env, encodings, 5, true) != 0;
|
|
}
|
|
|
|
/* Telephony and remote submix are not places to play to. */
|
|
if (list && get_name && to_string && id > 0 && type != 18 && type != 25)
|
|
{
|
|
char entry[256];
|
|
const char *name = NULL;
|
|
const char *kind = android_audio_device_type(type);
|
|
jobject cs = NULL;
|
|
jstring str = NULL;
|
|
CALL_OBJ_METHOD(env, cs, dev, get_name);
|
|
if (cs)
|
|
CALL_OBJ_METHOD(env, str, cs, to_string);
|
|
if (str)
|
|
name = (*env)->GetStringUTFChars(env, str, NULL);
|
|
if (kind)
|
|
snprintf(entry, sizeof(entry), "%d (%s, %s)", (int)id,
|
|
(name && *name) ? name : "Unknown", kind);
|
|
else
|
|
snprintf(entry, sizeof(entry), "%d (%s)", (int)id,
|
|
(name && *name) ? name : "Unknown");
|
|
if (name)
|
|
(*env)->ReleaseStringUTFChars(env, str, name);
|
|
string_list_append(list, entry, attr);
|
|
}
|
|
(*env)->PopLocalFrame(env, NULL);
|
|
}
|
|
|
|
done:
|
|
(*env)->PopLocalFrame(env, NULL);
|
|
return found;
|
|
}
|
|
|
|
struct string_list *frontend_android_audio_outputs_new(void)
|
|
{
|
|
struct string_list *list = string_list_new();
|
|
if (list)
|
|
android_audio_outputs_walk(list, 0, NULL, NULL);
|
|
return list;
|
|
}
|
|
|
|
bool frontend_android_audio_device_caps(int32_t id,
|
|
unsigned *max_channels, bool *ac3)
|
|
{
|
|
*max_channels = 0;
|
|
*ac3 = false;
|
|
if (id <= 0)
|
|
return false;
|
|
return android_audio_outputs_walk(NULL, id, max_channels, ac3);
|
|
}
|
|
|
|
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;
|
|
|
|
android_lifecycle_set_flags(&android_app->lc, ANDROID_LC_PERM_RESOLVED
|
|
| (granted ? ANDROID_LC_PERM_GRANTED : 0));
|
|
/* Against the app thread's store of the looper and its re-check of
|
|
* the flag: one side or the other sees the other's write. */
|
|
retro_atomic_thread_fence_seq_cst();
|
|
looper = (ALooper*)retro_atomic_load_acquire_ptr(
|
|
&android_app->looper_to_wake);
|
|
|
|
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"))
|
|
{
|
|
/* "<number> mAh": parse, then check what follows */
|
|
char *endptr = NULL;
|
|
long _val = strtol(val, &endptr, 10);
|
|
if (endptr != val && *endptr == ' ')
|
|
remaining = (int)_val;
|
|
}
|
|
}
|
|
|
|
ptr = &buf_info[0];
|
|
|
|
while (make_proc_acpi_key_val(&ptr, &key, &val))
|
|
{
|
|
if (string_is_equal(key, "design capacity"))
|
|
{
|
|
char *endptr = NULL;
|
|
long _val = strtol(val, &endptr, 10);
|
|
if (endptr != val && *endptr == ' ')
|
|
maximum = (int)_val;
|
|
}
|
|
}
|
|
|
|
/* maximum is the divisor */
|
|
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
|
|
|
|
#if !defined(ANDROID) && !defined(DINGUX)
|
|
/* Distribution packages install the shared libretro data sets under
|
|
* <prefix>/share/libretro/<name> (FreeBSD ports: retroarch-assets,
|
|
* libretro-core-info; Debian and its derivatives use the same layout).
|
|
* Default to those when present so a locally built RetroArch finds the
|
|
* packaged assets, core info, shaders and joypad profiles without any
|
|
* retroarch.cfg edits. Only what would otherwise fall back to an empty
|
|
* per-user directory is probed here; the per-user directory stays the
|
|
* default for anything writable. */
|
|
static bool unix_find_packaged_dir(char *s, size_t len, const char *name)
|
|
{
|
|
static const char *const prefixes[] = {
|
|
"/usr/local/share/libretro",
|
|
"/usr/share/libretro"
|
|
};
|
|
size_t i;
|
|
for (i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]); i++)
|
|
{
|
|
fill_pathname_join(s, prefixes[i], name, len);
|
|
if (path_is_directory(s))
|
|
return true;
|
|
}
|
|
*s = '\0';
|
|
return false;
|
|
}
|
|
#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 if (unix_find_packaged_dir(g_defaults.dirs[DEFAULT_DIR_CORE_INFO],
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_CORE_INFO]), "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]));
|
|
#if !defined(DINGUX)
|
|
else if (unix_find_packaged_dir(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG],
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_AUTOCONFIG]), "autoconfig"))
|
|
;
|
|
#endif
|
|
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]));
|
|
#if !defined(DINGUX)
|
|
else if (unix_find_packaged_dir(g_defaults.dirs[DEFAULT_DIR_ASSETS],
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_ASSETS]), "assets"))
|
|
;
|
|
#endif
|
|
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]));
|
|
#if !defined(DINGUX)
|
|
else if (unix_find_packaged_dir(g_defaults.dirs[DEFAULT_DIR_SHADER],
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_SHADER]), "shaders"))
|
|
;
|
|
#endif
|
|
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]));
|
|
}
|
|
|
|
#ifdef WEBOS
|
|
/* Start Load Content on the TV's shared storage, where users put
|
|
* their content. Only a fresh config picks this up; an existing
|
|
* retroarch.cfg keeps its saved rgui_browser_directory. */
|
|
if (path_is_directory("/media/internal"))
|
|
strlcpy(g_defaults.dirs[DEFAULT_DIR_MENU_CONTENT], "/media/internal",
|
|
sizeof(g_defaults.dirs[DEFAULT_DIR_MENU_CONTENT]));
|
|
#endif
|
|
#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)
|
|
{
|
|
if (android_app->savedState)
|
|
{
|
|
free(android_app->savedState);
|
|
android_app->savedState = NULL;
|
|
android_app->savedStateSize = 0;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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
|
|
|
|
android_input_reader_stop(android_app);
|
|
|
|
AConfiguration_delete(android_app->config);
|
|
android_lifecycle_set_flags(&android_app->lc, ANDROID_LC_DESTROYED);
|
|
/* Can't touch android_app object after this. */
|
|
}
|
|
#endif
|
|
|
|
#ifdef UNIX_GAMEMODE
|
|
static bool frontend_unix_set_gamemode(bool on)
|
|
{
|
|
return gamemode_linux_set(on, 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
|
|
|
|
#ifdef UNIX_GAMEMODE
|
|
gamemode_linux_set(false, true);
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
|
|
#ifdef HAVE_AAUDIO
|
|
/* AAudio is Android 8.0 (API 26) and up. */
|
|
{
|
|
int32_t sdk = 0;
|
|
frontend_android_get_version_sdk(&sdk);
|
|
if (sdk < 26)
|
|
audio_driver_list_remove(&audio_aaudio);
|
|
}
|
|
#endif
|
|
|
|
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);
|
|
android_app->looper = looper;
|
|
/* Held for the UI thread's wakes until android_app_free() */
|
|
ALooper_acquire(looper);
|
|
retro_atomic_store_release_ptr(&android_app->looper_to_wake, looper);
|
|
retro_atomic_thread_fence_seq_cst();
|
|
android_lifecycle_set_flags(&android_app->lc, ANDROID_LC_RUNNING);
|
|
|
|
memset(&g_android, 0, sizeof(g_android));
|
|
g_android = (struct android_app*)android_app;
|
|
|
|
/* Before the first command is read: it may hand over the input
|
|
* queue, which only the reader reads */
|
|
if (!android_input_reader_start(android_app))
|
|
{
|
|
frontend_unix_deinit(android_app);
|
|
frontend_android_shutdown(android_app);
|
|
return;
|
|
}
|
|
|
|
while (!android_app_window(android_app))
|
|
{
|
|
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). Settings may remove the
|
|
* window, so wait for its replacement as well. A launch with the
|
|
* permission already settled signals before this loop is reached
|
|
* and never waits. */
|
|
for (;;)
|
|
{
|
|
bool resolved;
|
|
|
|
resolved = (android_lifecycle_flags(&android_app->lc)
|
|
& ANDROID_LC_PERM_RESOLVED) != 0;
|
|
if (resolved && android_app_window(android_app))
|
|
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->getHdrMaxLuminance, class,
|
|
"getHdrMaxLuminance", "()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->isMetaQuestBuild, class,
|
|
"isMetaQuestBuild", "()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");
|
|
GET_METHOD_ID(env, android_app->setSecondDisplay, class,
|
|
"setSecondDisplay", "(II)V");
|
|
|
|
CALL_BOOLEAN_METHOD(env, android_app->is_play_store_build,
|
|
android_app->activity->clazz, android_app->isPlayStoreBuild);
|
|
CALL_BOOLEAN_METHOD(env, android_app->is_meta_quest_build,
|
|
android_app->activity->clazz, android_app->isMetaQuestBuild);
|
|
#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
|
|
}
|
|
|
|
/* Whether the drive list offers the filesystem root. Play Store
|
|
* Android builds cannot read it under scoped storage, and on webOS
|
|
* the jailed root is not user storage - listing it has frozen the
|
|
* file browser - so both offer only their real storage locations. */
|
|
#ifdef HAVE_MENU
|
|
static bool frontend_unix_root_in_drive_list(void)
|
|
{
|
|
#if defined(ANDROID)
|
|
return !g_android->is_play_store_build;
|
|
#elif defined(WEBOS)
|
|
return false;
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
#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;
|
|
int i;
|
|
/* 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 (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
|
|
|
|
if (frontend_unix_root_in_drive_list())
|
|
{
|
|
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)
|
|
{
|
|
int quit;
|
|
#ifdef VALGRIND_PRINTF_BACKTRACE
|
|
VALGRIND_PRINTF_BACKTRACE("SIGINT");
|
|
#endif
|
|
(void)sig;
|
|
quit = retro_atomic_fetch_add_int(&unix_sighandler_quit, 1) + 1;
|
|
if (quit == 1)
|
|
{
|
|
#if defined(HAVE_SDL_DINGUX)
|
|
retroarch_ctl(RARCH_CTL_SET_SHUTDOWN, NULL);
|
|
#endif
|
|
}
|
|
/* _exit, not exit: exit() runs atexit handlers and frees memory,
|
|
* which deadlocks on the allocator's lock when this signal lands
|
|
* inside free() - as it does during a slow shutdown, the very case
|
|
* a second Ctrl+C is meant to force. */
|
|
if (quit == 2) _exit(1);
|
|
/* in case there's a second deadlock in a C++ destructor or something */
|
|
if (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 retro_atomic_load_acquire_int(&unix_sighandler_quit);
|
|
}
|
|
|
|
static void frontend_unix_set_signal_handler_state(int value)
|
|
{
|
|
retro_atomic_store_release_int(&unix_sighandler_quit, value);
|
|
}
|
|
|
|
static void frontend_unix_destroy_signal_handler_state(void)
|
|
{
|
|
retro_atomic_store_release_int(&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 void narrator_reap_stopping_unix(void)
|
|
{
|
|
unsigned i;
|
|
for (i = 0; i < NARRATOR_STOPPING_MAX; i++)
|
|
if (speak_stopping[i] > 0 && waitpid(speak_stopping[i], NULL, WNOHANG) != 0)
|
|
speak_stopping[i] = 0;
|
|
}
|
|
|
|
/* Running while waitpid finds it unfinished; once it has exited it is
|
|
* reaped here, so a finished narrator never reads as running and its
|
|
* pid is never kept past its exit. */
|
|
static bool is_narrator_running_unix(void)
|
|
{
|
|
narrator_reap_stopping_unix();
|
|
if (speak_pid <= 0)
|
|
return false;
|
|
if (waitpid(speak_pid, NULL, WNOHANG) == 0)
|
|
return true;
|
|
speak_pid = 0;
|
|
return false;
|
|
}
|
|
|
|
/* SIGTERM to the running narrator, reaped now if it has gone, else kept
|
|
* to reap on a later call: nothing here waits. */
|
|
static void narrator_stop_unix(void)
|
|
{
|
|
unsigned i;
|
|
if (speak_pid <= 0)
|
|
return;
|
|
kill(speak_pid, SIGTERM);
|
|
if (waitpid(speak_pid, NULL, WNOHANG) == 0)
|
|
{
|
|
narrator_reap_stopping_unix();
|
|
for (i = 0; i < NARRATOR_STOPPING_MAX; i++)
|
|
if (speak_stopping[i] <= 0)
|
|
{
|
|
speak_stopping[i] = speak_pid;
|
|
break;
|
|
}
|
|
}
|
|
speak_pid = 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);
|
|
|
|
/* a lower-priority message waits for the running narrator */
|
|
if (priority < 10 && is_narrator_running_unix())
|
|
goto end;
|
|
|
|
narrator_stop_unix();
|
|
|
|
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: the narrator is reaped by
|
|
* is_narrator_running_unix() and narrator_stop_unix() */
|
|
if (pid > 0)
|
|
speak_pid = pid;
|
|
}
|
|
}
|
|
|
|
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 UNIX_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 */
|
|
#ifdef HAVE_MENU
|
|
frontend_unix_root_in_drive_list /* root_in_drive_list */
|
|
#else
|
|
NULL /* root_in_drive_list */
|
|
#endif
|
|
};
|