mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
added log option and fix ui switching orientation problems.. make ui faster also vsync option fix
This commit is contained in:
@@ -53,12 +53,10 @@
|
||||
|
||||
<activity
|
||||
android:name=".GameLibraryActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdarg>
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
@@ -36,34 +38,68 @@ extern "C" AddfdInfo* monitor_fdset_add_fd(int fd, bool has_fdset_id,
|
||||
namespace {
|
||||
constexpr const char* kLogTag = "xemu-android";
|
||||
constexpr const char* kPrefsName = "x1box_prefs";
|
||||
constexpr const char* kDebugLogPrefKey = "setting_debug_logs_enabled";
|
||||
constexpr const char* kDebugLogRelativeDir = "x1box/debug-logs";
|
||||
constexpr const char* kNativeDebugLogFileName = "xemu-debug.log";
|
||||
constexpr off_t kMaxDebugLogBytes = 4 * 1024 * 1024;
|
||||
static std::atomic<bool> g_qemu_init_started{false};
|
||||
static std::atomic<bool> g_native_debug_logging_enabled{false};
|
||||
static std::string g_native_debug_log_path;
|
||||
static SDL_mutex* g_native_debug_log_mutex = nullptr;
|
||||
|
||||
static JNIEnv* GetEnv();
|
||||
static jobject GetActivity(JNIEnv* env);
|
||||
static bool HasException(JNIEnv* env, const char* context);
|
||||
static std::string GetFilesDirPath(JNIEnv* env, jobject activity);
|
||||
static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity);
|
||||
static bool NativeDebugLoggingEnabled();
|
||||
static void AppendNativeDebugLog(const char* level, const char* message);
|
||||
|
||||
static void LogInfo(const char* msg) {
|
||||
if (!NativeDebugLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
__android_log_print(ANDROID_LOG_INFO, kLogTag, "%s", msg);
|
||||
AppendNativeDebugLog("I", msg);
|
||||
}
|
||||
|
||||
static void LogInfoFmt(const char* fmt, const char* detail) {
|
||||
if (!NativeDebugLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
__android_log_print(ANDROID_LOG_INFO, kLogTag, fmt, detail);
|
||||
char buffer[1024] = {};
|
||||
std::snprintf(buffer, sizeof(buffer), fmt, detail);
|
||||
AppendNativeDebugLog("I", buffer);
|
||||
}
|
||||
|
||||
static void LogInfoInt(const char* fmt, int value) {
|
||||
if (!NativeDebugLoggingEnabled()) {
|
||||
return;
|
||||
}
|
||||
__android_log_print(ANDROID_LOG_INFO, kLogTag, fmt, value);
|
||||
char buffer[1024] = {};
|
||||
std::snprintf(buffer, sizeof(buffer), fmt, value);
|
||||
AppendNativeDebugLog("I", buffer);
|
||||
}
|
||||
|
||||
static void LogError(const char* msg) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "%s", msg);
|
||||
AppendNativeDebugLog("E", msg);
|
||||
}
|
||||
|
||||
static void LogErrorInt(const char* fmt, int value) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, kLogTag, fmt, value);
|
||||
char buffer[1024] = {};
|
||||
std::snprintf(buffer, sizeof(buffer), fmt, value);
|
||||
AppendNativeDebugLog("E", buffer);
|
||||
}
|
||||
|
||||
static void LogErrorFmt(const char* fmt, const char* detail) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, kLogTag, fmt, detail);
|
||||
char buffer[1024] = {};
|
||||
std::snprintf(buffer, sizeof(buffer), fmt, detail);
|
||||
AppendNativeDebugLog("E", buffer);
|
||||
}
|
||||
|
||||
static int g_next_dvd_fdset_id = 9000;
|
||||
@@ -80,6 +116,43 @@ static bool FileExists(const std::string& path) {
|
||||
return stat(path.c_str(), &st) == 0;
|
||||
}
|
||||
|
||||
static std::string CurrentNativeLogTimestamp() {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm local_time {};
|
||||
localtime_r(&now, &local_time);
|
||||
char buffer[32] = {};
|
||||
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &local_time);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
static bool NativeDebugLoggingEnabled() {
|
||||
return g_native_debug_logging_enabled.load() && !g_native_debug_log_path.empty();
|
||||
}
|
||||
|
||||
static void AppendNativeDebugLog(const char* level, const char* message) {
|
||||
if (!NativeDebugLoggingEnabled() || !level || !message || !g_native_debug_log_mutex) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_LockMutex(g_native_debug_log_mutex);
|
||||
|
||||
struct stat st {};
|
||||
const bool should_truncate =
|
||||
stat(g_native_debug_log_path.c_str(), &st) == 0 &&
|
||||
st.st_size > kMaxDebugLogBytes;
|
||||
|
||||
std::ofstream out(
|
||||
g_native_debug_log_path,
|
||||
should_truncate ? (std::ios::out | std::ios::trunc)
|
||||
: (std::ios::out | std::ios::app));
|
||||
if (out.is_open()) {
|
||||
out << CurrentNativeLogTimestamp() << ' ' << level << '/' << kLogTag
|
||||
<< ": " << message << '\n';
|
||||
}
|
||||
|
||||
SDL_UnlockMutex(g_native_debug_log_mutex);
|
||||
}
|
||||
|
||||
static bool IsTcgTuningEnabled() {
|
||||
const char* value = SDL_getenv("XEMU_ANDROID_TCG_TUNING");
|
||||
return !(value && value[0] == '0');
|
||||
@@ -261,6 +334,57 @@ static std::string JStringToString(JNIEnv* env, jstring value) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string GetFilesDirPath(JNIEnv* env, jobject activity) {
|
||||
if (!env || !activity) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (!activityClass) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jmethodID getFilesDir =
|
||||
env->GetMethodID(activityClass, "getFilesDir", "()Ljava/io/File;");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (!getFilesDir) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jobject fileObj = env->CallObjectMethod(activity, getFilesDir);
|
||||
if (HasException(env, "Activity.getFilesDir") || !fileObj) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jclass fileClass = env->GetObjectClass(fileObj);
|
||||
if (!fileClass) {
|
||||
env->DeleteLocalRef(fileObj);
|
||||
return {};
|
||||
}
|
||||
|
||||
jmethodID getAbsolutePath =
|
||||
env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;");
|
||||
if (!getAbsolutePath) {
|
||||
env->DeleteLocalRef(fileClass);
|
||||
env->DeleteLocalRef(fileObj);
|
||||
return {};
|
||||
}
|
||||
|
||||
jstring pathValue = static_cast<jstring>(
|
||||
env->CallObjectMethod(fileObj, getAbsolutePath));
|
||||
std::string path;
|
||||
if (!HasException(env, "File.getAbsolutePath")) {
|
||||
path = JStringToString(env, pathValue);
|
||||
}
|
||||
|
||||
if (pathValue) {
|
||||
env->DeleteLocalRef(pathValue);
|
||||
}
|
||||
env->DeleteLocalRef(fileClass);
|
||||
env->DeleteLocalRef(fileObj);
|
||||
return path;
|
||||
}
|
||||
|
||||
static bool HasInlineAioCrashFlag(const std::string& flag_path) {
|
||||
if (flag_path.empty()) {
|
||||
return false;
|
||||
@@ -427,6 +551,31 @@ static int GetPrefInt(JNIEnv* env, jobject activity, const char* key, int defVal
|
||||
return out;
|
||||
}
|
||||
|
||||
static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity) {
|
||||
g_native_debug_logging_enabled.store(
|
||||
GetPrefBool(env, activity, kDebugLogPrefKey, false));
|
||||
g_native_debug_log_path.clear();
|
||||
|
||||
if (!g_native_debug_logging_enabled.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string files_dir = GetFilesDirPath(env, activity);
|
||||
if (files_dir.empty()) {
|
||||
g_native_debug_logging_enabled.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string log_dir = files_dir + "/" + kDebugLogRelativeDir;
|
||||
EnsureDirExists(files_dir + "/x1box");
|
||||
EnsureDirExists(log_dir);
|
||||
g_native_debug_log_path = log_dir + "/" + kNativeDebugLogFileName;
|
||||
|
||||
if (!g_native_debug_log_mutex) {
|
||||
g_native_debug_log_mutex = SDL_CreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsSeekableFd(int fd) {
|
||||
errno = 0;
|
||||
return lseek(fd, 0, SEEK_CUR) != static_cast<off_t>(-1);
|
||||
@@ -723,7 +872,7 @@ struct EmulatorSettings {
|
||||
bool hrtf = true;
|
||||
bool cache_shaders = true;
|
||||
bool hard_fpu = true;
|
||||
bool vsync = true;
|
||||
bool vsync = false;
|
||||
bool skip_boot_anim = false;
|
||||
bool network_enabled = false;
|
||||
};
|
||||
@@ -990,7 +1139,7 @@ static SetupFiles SyncSetupFiles() {
|
||||
emuSettings.filtering = "nearest";
|
||||
}
|
||||
}
|
||||
emuSettings.vsync = GetPrefBool(env, activity, "setting_vsync", true);
|
||||
emuSettings.vsync = GetPrefBool(env, activity, "setting_vsync", false);
|
||||
out.audio_driver = GetPrefString(env, activity, "setting_audio_driver");
|
||||
{
|
||||
std::string normalized = ToLowerAscii(out.audio_driver);
|
||||
@@ -1070,6 +1219,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
ConfigureNativeDebugLogging(GetEnv(), GetActivity(GetEnv()));
|
||||
LogInfo("SDL_main: start");
|
||||
std::string audio_driver_hint = ResolveAndroidAudioDriverHint();
|
||||
SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, audio_driver_hint.c_str(),
|
||||
|
||||
@@ -81,7 +81,7 @@ static void xemu_settings_apply_defaults(void)
|
||||
CONFIG_DISPLAY_WINDOW_STARTUP_SIZE_1280X960;
|
||||
g_config.display.window.last_width = 640;
|
||||
g_config.display.window.last_height = 480;
|
||||
g_config.display.window.vsync = true;
|
||||
g_config.display.window.vsync = false;
|
||||
g_config.display.ui.show_menubar = true;
|
||||
g_config.display.ui.show_notifications = true;
|
||||
g_config.display.ui.hide_cursor = true;
|
||||
|
||||
@@ -33,7 +33,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on button press: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "Error on button press: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on button release: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "Error on button release: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on stick move: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "Error on stick move: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
}
|
||||
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on stick press: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "Error on stick press: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
}
|
||||
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on stick release: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "Error on stick release: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +113,12 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "SDL pad event failed for $button: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "SDL pad event failed for $button: ${e.message}" }
|
||||
}
|
||||
try {
|
||||
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, axis, axisValue)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "SDL joy event failed for $button: ${e.message}")
|
||||
DebugLog.e("ControllerBridge", e) { "SDL joy event failed for $button: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object DebugLog {
|
||||
const val PREF_ENABLED = "setting_debug_logs_enabled"
|
||||
|
||||
private const val TAG = "xemu-android"
|
||||
private const val LOG_DIR = "x1box/debug-logs"
|
||||
private const val UI_LOG_FILE_NAME = "ui-debug.log"
|
||||
private const val NATIVE_LOG_FILE_NAME = "xemu-debug.log"
|
||||
private const val UI_LOGCAT_FILE_NAME = "ui-logcat.log"
|
||||
private const val XEMU_LOGCAT_FILE_NAME = "xemu-logcat.log"
|
||||
private const val MAX_LOG_BYTES = 16L * 1024L * 1024L
|
||||
|
||||
@Volatile private var appContext: Context? = null
|
||||
@PublishedApi
|
||||
@Volatile
|
||||
internal var enabled = false
|
||||
@Volatile private var logcatProcess: java.lang.Process? = null
|
||||
@Volatile private var logcatThread: Thread? = null
|
||||
@Volatile private var activeLogcatPath: String? = null
|
||||
|
||||
private val writerExecutor = Executors.newSingleThreadExecutor { runnable ->
|
||||
Thread(runnable, "x1box-debug-log-writer").apply {
|
||||
isDaemon = true
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize(context: Context) {
|
||||
val applicationContext = context.applicationContext
|
||||
appContext = applicationContext
|
||||
enabled = applicationContext
|
||||
.getSharedPreferences("x1box_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean(PREF_ENABLED, false)
|
||||
ensureLogcatCaptureState(applicationContext)
|
||||
}
|
||||
|
||||
fun setEnabled(context: Context, value: Boolean, resetLogs: Boolean = false) {
|
||||
initialize(context)
|
||||
if (!value) {
|
||||
stopLogcatCapture()
|
||||
}
|
||||
if (resetLogs) {
|
||||
clearLogs(context)
|
||||
}
|
||||
enabled = value
|
||||
if (value) {
|
||||
ensureLogcatCaptureState(context.applicationContext)
|
||||
}
|
||||
if (value) {
|
||||
i(TAG) { "Debug logging enabled" }
|
||||
} else {
|
||||
Log.i(TAG, "Debug logging disabled")
|
||||
}
|
||||
}
|
||||
|
||||
fun hasAnyLog(context: Context): Boolean {
|
||||
return uiLogFile(context).isFile ||
|
||||
nativeLogFile(context).isFile ||
|
||||
uiLogcatFile(context).isFile ||
|
||||
xemuLogcatFile(context).isFile
|
||||
}
|
||||
|
||||
fun exportDefaultFileName(): String {
|
||||
val stamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
|
||||
return "x1box-debug-$stamp.log"
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun exportCombined(context: Context, outputStream: OutputStream) {
|
||||
val uiLog = uiLogFile(context)
|
||||
val nativeLog = nativeLogFile(context)
|
||||
if (!uiLog.isFile && !nativeLog.isFile) {
|
||||
throw IllegalStateException("No debug log captured yet.")
|
||||
}
|
||||
|
||||
outputStream.bufferedWriter().use { writer ->
|
||||
if (uiLog.isFile) {
|
||||
writer.appendLine("=== UI Debug Log ===")
|
||||
uiLog.bufferedReader().useLines { lines ->
|
||||
lines.forEach(writer::appendLine)
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeLog.isFile) {
|
||||
if (uiLog.isFile) {
|
||||
writer.appendLine()
|
||||
}
|
||||
writer.appendLine("=== xemu Native Debug Log ===")
|
||||
nativeLog.bufferedReader().useLines { lines ->
|
||||
lines.forEach(writer::appendLine)
|
||||
}
|
||||
}
|
||||
|
||||
val uiLogcat = uiLogcatFile(context)
|
||||
if (uiLogcat.isFile) {
|
||||
writer.appendLine()
|
||||
writer.appendLine("=== UI Logcat Capture ===")
|
||||
uiLogcat.bufferedReader().useLines { lines ->
|
||||
lines.forEach(writer::appendLine)
|
||||
}
|
||||
}
|
||||
|
||||
val xemuLogcat = xemuLogcatFile(context)
|
||||
if (xemuLogcat.isFile) {
|
||||
writer.appendLine()
|
||||
writer.appendLine("=== xemu Logcat Capture ===")
|
||||
xemuLogcat.bufferedReader().useLines { lines ->
|
||||
lines.forEach(writer::appendLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearLogs(context: Context? = appContext) {
|
||||
context ?: return
|
||||
stopLogcatCapture()
|
||||
uiLogFile(context).delete()
|
||||
nativeLogFile(context).delete()
|
||||
uiLogcatFile(context).delete()
|
||||
xemuLogcatFile(context).delete()
|
||||
}
|
||||
|
||||
inline fun d(tag: String, message: () -> String) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
val text = message()
|
||||
Log.d(tag, text)
|
||||
appendUiLine("D", tag, text)
|
||||
}
|
||||
|
||||
inline fun i(tag: String, message: () -> String) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
val text = message()
|
||||
Log.i(tag, text)
|
||||
appendUiLine("I", tag, text)
|
||||
}
|
||||
|
||||
inline fun w(tag: String, message: () -> String) {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
val text = message()
|
||||
Log.w(tag, text)
|
||||
appendUiLine("W", tag, text)
|
||||
}
|
||||
|
||||
inline fun e(tag: String, throwable: Throwable? = null, message: () -> String) {
|
||||
val text = message()
|
||||
if (throwable != null) {
|
||||
Log.e(tag, text, throwable)
|
||||
} else {
|
||||
Log.e(tag, text)
|
||||
}
|
||||
if (enabled) {
|
||||
appendUiLine("E", tag, text, throwable)
|
||||
}
|
||||
}
|
||||
|
||||
fun nativeLogFile(context: Context): File {
|
||||
return File(logDir(context), NATIVE_LOG_FILE_NAME)
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal fun appendUiLine(
|
||||
level: String,
|
||||
tag: String,
|
||||
message: String,
|
||||
throwable: Throwable? = null,
|
||||
) {
|
||||
val context = appContext ?: return
|
||||
writerExecutor.execute {
|
||||
try {
|
||||
val file = uiLogFile(context)
|
||||
file.parentFile?.mkdirs()
|
||||
trimFileIfNeeded(file)
|
||||
file.appendText(
|
||||
buildString {
|
||||
append(timestamp())
|
||||
append(' ')
|
||||
append(level)
|
||||
append('/')
|
||||
append(tag)
|
||||
append(": ")
|
||||
appendLine(message)
|
||||
if (throwable != null) {
|
||||
appendLine(stackTraceFor(throwable))
|
||||
}
|
||||
},
|
||||
Charsets.UTF_8
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun uiLogFile(context: Context): File {
|
||||
return File(logDir(context), UI_LOG_FILE_NAME)
|
||||
}
|
||||
|
||||
private fun uiLogcatFile(context: Context): File {
|
||||
return File(logDir(context), UI_LOGCAT_FILE_NAME)
|
||||
}
|
||||
|
||||
private fun xemuLogcatFile(context: Context): File {
|
||||
return File(logDir(context), XEMU_LOGCAT_FILE_NAME)
|
||||
}
|
||||
|
||||
private fun currentProcessLogcatFile(context: Context): File {
|
||||
val processName = runCatching {
|
||||
File("/proc/self/cmdline").readText().trim('\u0000', ' ', '\n')
|
||||
}.getOrDefault("")
|
||||
return if (processName.endsWith(":xemu")) {
|
||||
xemuLogcatFile(context)
|
||||
} else {
|
||||
uiLogcatFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logDir(context: Context): File {
|
||||
return File(context.filesDir, LOG_DIR)
|
||||
}
|
||||
|
||||
private fun trimFileIfNeeded(file: File) {
|
||||
if (file.isFile && file.length() > MAX_LOG_BYTES) {
|
||||
file.writeText("")
|
||||
}
|
||||
}
|
||||
|
||||
private fun timestamp(): String {
|
||||
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US).format(Date())
|
||||
}
|
||||
|
||||
private fun stackTraceFor(throwable: Throwable): String {
|
||||
return StringWriter().also { writer ->
|
||||
PrintWriter(writer).use { printer ->
|
||||
throwable.printStackTrace(printer)
|
||||
}
|
||||
}.toString().trimEnd()
|
||||
}
|
||||
|
||||
private fun ensureLogcatCaptureState(context: Context) {
|
||||
if (!enabled) {
|
||||
stopLogcatCapture()
|
||||
return
|
||||
}
|
||||
|
||||
val targetFile = currentProcessLogcatFile(context)
|
||||
if (activeLogcatPath == targetFile.absolutePath && logcatProcess != null) {
|
||||
return
|
||||
}
|
||||
|
||||
stopLogcatCapture()
|
||||
startLogcatCapture(targetFile)
|
||||
}
|
||||
|
||||
private fun startLogcatCapture(targetFile: File) {
|
||||
try {
|
||||
targetFile.parentFile?.mkdirs()
|
||||
trimFileIfNeeded(targetFile)
|
||||
val process = ProcessBuilder(
|
||||
"logcat",
|
||||
"-T",
|
||||
"1",
|
||||
"-v",
|
||||
"threadtime",
|
||||
"--pid=${android.os.Process.myPid()}",
|
||||
)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
val thread = Thread({
|
||||
try {
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
FileOutputStream(targetFile, true).bufferedWriter(Charsets.UTF_8).use { writer ->
|
||||
while (true) {
|
||||
val line = reader.readLine() ?: break
|
||||
writer.appendLine(line)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}, "x1box-logcat-capture").apply {
|
||||
isDaemon = true
|
||||
start()
|
||||
}
|
||||
|
||||
logcatProcess = process
|
||||
logcatThread = thread
|
||||
activeLogcatPath = targetFile.absolutePath
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "Failed to start logcat capture", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopLogcatCapture() {
|
||||
logcatProcess?.destroy()
|
||||
logcatProcess = null
|
||||
logcatThread?.interrupt()
|
||||
logcatThread = null
|
||||
activeLogcatPath = null
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
class GameLibraryActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
const val EXTRA_RESTART_LAST_GAME = "com.izzy2lost.x1box.extra.RESTART_LAST_GAME"
|
||||
const val EXTRA_INITIAL_ORIENTATION =
|
||||
"com.izzy2lost.x1box.extra.INITIAL_ORIENTATION"
|
||||
private const val SNAPSHOT_PREVIEW_HEADER_SIZE = 12
|
||||
private const val TOTAL_SNAPSHOT_SLOTS = 10
|
||||
private const val XDVDFS_SECTOR_SIZE = 2048L
|
||||
@@ -101,7 +103,12 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
private val coverIndex = ConcurrentHashMap<String, String>()
|
||||
private val coverCollapsedIndex = ConcurrentHashMap<String, String>()
|
||||
private val coverEntries = ArrayList<CoverEntry>()
|
||||
private val coverIndexLock = Any()
|
||||
private val discFormatCacheLock = Any()
|
||||
private val discFormatCache = HashMap<String, DiscImageFormat>()
|
||||
@Volatile private var coverIndexLoaded = false
|
||||
@Volatile private var discFormatCacheLoaded = false
|
||||
@Volatile private var discFormatCacheDirty = false
|
||||
|
||||
private lateinit var loadingSpinner: ProgressBar
|
||||
private lateinit var loadingText: TextView
|
||||
@@ -148,6 +155,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applyInitialOrientationFromIntent()
|
||||
OrientationLocker(this).enable()
|
||||
setContentView(R.layout.activity_game_library)
|
||||
EdgeToEdgeHelper.enable(this)
|
||||
@@ -188,7 +196,11 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
launchDashboard()
|
||||
}
|
||||
btnSettings.setOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
startActivity(
|
||||
Intent(this, SettingsActivity::class.java).apply {
|
||||
putExtra(SettingsActivity.EXTRA_INITIAL_ORIENTATION, requestedOrientation)
|
||||
}
|
||||
)
|
||||
}
|
||||
btnSnapshots.setOnClickListener {
|
||||
showSnapshotStartupPicker()
|
||||
@@ -219,6 +231,15 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
loadGames()
|
||||
}
|
||||
|
||||
private fun applyInitialOrientationFromIntent() {
|
||||
val initialOrientation = intent.getIntExtra(EXTRA_INITIAL_ORIENTATION, Int.MIN_VALUE)
|
||||
if (initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
|
||||
initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
) {
|
||||
requestedOrientation = initialOrientation
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryRestartLastGameFromIntent(): Boolean {
|
||||
if (!intent.getBooleanExtra(EXTRA_RESTART_LAST_GAME, false)) {
|
||||
return false
|
||||
@@ -539,6 +560,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
|
||||
val generation = ++scanGeneration
|
||||
Thread {
|
||||
loadDiscFormatCacheIfNeeded()
|
||||
val games = scanFolderForGames(readyFolderUri)
|
||||
runOnUiThread {
|
||||
if (generation != scanGeneration) {
|
||||
@@ -813,47 +835,52 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
if (coverIndexLoaded) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val lines = assets.open("X1_Covers.txt").bufferedReader().use { it.readLines() }
|
||||
val seenEntries = HashSet<String>()
|
||||
for (line in lines) {
|
||||
val fileName = line.trim()
|
||||
if (fileName.isEmpty() || !fileName.endsWith(".png", ignoreCase = true)) {
|
||||
continue
|
||||
}
|
||||
val gameName = fileName.removeSuffix(".png").trim()
|
||||
val encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
|
||||
val url = coverRepoBaseUrl + encoded
|
||||
|
||||
val exactKey = normalizeCoverKey(gameName)
|
||||
val strippedKey = stripTrailingRegion(exactKey)
|
||||
if (exactKey.isNotEmpty()) {
|
||||
coverIndex.putIfAbsent(exactKey, url)
|
||||
}
|
||||
if (strippedKey.isNotEmpty()) {
|
||||
coverIndex.putIfAbsent(strippedKey, url)
|
||||
}
|
||||
val canonical = if (strippedKey.isNotEmpty()) strippedKey else exactKey
|
||||
val collapsed = collapseCoverKey(canonical)
|
||||
if (collapsed.isNotEmpty()) {
|
||||
coverCollapsedIndex.putIfAbsent(collapsed, url)
|
||||
}
|
||||
if (canonical.isNotEmpty() && seenEntries.add("$canonical|$url")) {
|
||||
val tokens = tokenizeCoverKey(canonical)
|
||||
coverEntries.add(
|
||||
CoverEntry(
|
||||
collapsed = collapsed,
|
||||
tokens = tokens,
|
||||
numericTokens = tokens.filterTo(HashSet()) { token -> token.any(Char::isDigit) },
|
||||
url = url
|
||||
)
|
||||
)
|
||||
}
|
||||
synchronized(coverIndexLock) {
|
||||
if (coverIndexLoaded) {
|
||||
return
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Keep empty index; grid will show placeholders if the asset is unavailable.
|
||||
try {
|
||||
val lines = assets.open("X1_Covers.txt").bufferedReader().use { it.readLines() }
|
||||
val seenEntries = HashSet<String>()
|
||||
for (line in lines) {
|
||||
val fileName = line.trim()
|
||||
if (fileName.isEmpty() || !fileName.endsWith(".png", ignoreCase = true)) {
|
||||
continue
|
||||
}
|
||||
val gameName = fileName.removeSuffix(".png").trim()
|
||||
val encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
|
||||
val url = coverRepoBaseUrl + encoded
|
||||
|
||||
val exactKey = normalizeCoverKey(gameName)
|
||||
val strippedKey = stripTrailingRegion(exactKey)
|
||||
if (exactKey.isNotEmpty()) {
|
||||
coverIndex.putIfAbsent(exactKey, url)
|
||||
}
|
||||
if (strippedKey.isNotEmpty()) {
|
||||
coverIndex.putIfAbsent(strippedKey, url)
|
||||
}
|
||||
val canonical = if (strippedKey.isNotEmpty()) strippedKey else exactKey
|
||||
val collapsed = collapseCoverKey(canonical)
|
||||
if (collapsed.isNotEmpty()) {
|
||||
coverCollapsedIndex.putIfAbsent(collapsed, url)
|
||||
}
|
||||
if (canonical.isNotEmpty() && seenEntries.add("$canonical|$url")) {
|
||||
val tokens = tokenizeCoverKey(canonical)
|
||||
coverEntries.add(
|
||||
CoverEntry(
|
||||
collapsed = collapsed,
|
||||
tokens = tokens,
|
||||
numericTokens = tokens.filterTo(HashSet()) { token -> token.any(Char::isDigit) },
|
||||
url = url
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Keep empty index; grid will show placeholders if the asset is unavailable.
|
||||
}
|
||||
coverIndexLoaded = true
|
||||
}
|
||||
coverIndexLoaded = true
|
||||
}
|
||||
|
||||
private fun normalizeLookupTitle(input: String): String {
|
||||
@@ -1249,6 +1276,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
stack.add(root to "")
|
||||
|
||||
val games = ArrayList<GameEntry>()
|
||||
val seenDiscFormatKeys = HashSet<String>()
|
||||
while (stack.isNotEmpty()) {
|
||||
val (node, prefix) = stack.removeLast()
|
||||
val files = try {
|
||||
@@ -1265,22 +1293,128 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
if (!child.isFile || !isSupportedGame(name)) {
|
||||
continue
|
||||
}
|
||||
val sizeBytes = child.length()
|
||||
games.add(
|
||||
GameEntry(
|
||||
title = toGameTitle(name),
|
||||
uri = child.uri,
|
||||
relativePath = prefix + name,
|
||||
sizeBytes = child.length(),
|
||||
discImageFormat = detectDiscImageFormat(child.uri, name),
|
||||
sizeBytes = sizeBytes,
|
||||
discImageFormat = resolveDiscImageFormat(child.uri, name, sizeBytes, seenDiscFormatKeys),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
games.sortBy { it.title.lowercase(Locale.ROOT) }
|
||||
persistDiscFormatCache(seenDiscFormatKeys)
|
||||
return games
|
||||
}
|
||||
|
||||
private fun discFormatCacheFile(): File = File(filesDir, "disc_format_cache.tsv")
|
||||
|
||||
private fun loadDiscFormatCacheIfNeeded() {
|
||||
if (discFormatCacheLoaded) {
|
||||
return
|
||||
}
|
||||
synchronized(discFormatCacheLock) {
|
||||
if (discFormatCacheLoaded) {
|
||||
return
|
||||
}
|
||||
val cacheFile = discFormatCacheFile()
|
||||
if (cacheFile.isFile) {
|
||||
runCatching {
|
||||
cacheFile.forEachLine(Charsets.UTF_8) { line ->
|
||||
val split = line.lastIndexOf('\t')
|
||||
if (split <= 0 || split >= line.lastIndex) {
|
||||
return@forEachLine
|
||||
}
|
||||
val key = line.substring(0, split)
|
||||
val formatName = line.substring(split + 1)
|
||||
runCatching { DiscImageFormat.valueOf(formatName) }
|
||||
.getOrNull()
|
||||
?.let { format -> discFormatCache[key] = format }
|
||||
}
|
||||
}
|
||||
}
|
||||
discFormatCacheLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistDiscFormatCache(seenKeys: Set<String>) {
|
||||
synchronized(discFormatCacheLock) {
|
||||
if (!discFormatCacheLoaded) {
|
||||
return
|
||||
}
|
||||
|
||||
var changed = discFormatCacheDirty
|
||||
val iterator = discFormatCache.keys.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next() !in seenKeys) {
|
||||
iterator.remove()
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return
|
||||
}
|
||||
|
||||
val cacheFile = discFormatCacheFile()
|
||||
if (discFormatCache.isEmpty()) {
|
||||
cacheFile.delete()
|
||||
discFormatCacheDirty = false
|
||||
return
|
||||
}
|
||||
|
||||
runCatching {
|
||||
cacheFile.parentFile?.mkdirs()
|
||||
cacheFile.bufferedWriter(Charsets.UTF_8).use { writer ->
|
||||
discFormatCache.entries.forEach { (key, format) ->
|
||||
writer.append(key)
|
||||
writer.append('\t')
|
||||
writer.append(format.name)
|
||||
writer.append('\n')
|
||||
}
|
||||
}
|
||||
discFormatCacheDirty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun discFormatCacheKey(uri: Uri, sizeBytes: Long): String =
|
||||
uri.toString() + "\t" + sizeBytes
|
||||
|
||||
private fun resolveDiscImageFormat(
|
||||
uri: Uri,
|
||||
fileName: String,
|
||||
sizeBytes: Long,
|
||||
seenKeys: MutableSet<String>,
|
||||
): DiscImageFormat {
|
||||
val lower = fileName.lowercase(Locale.ROOT)
|
||||
if (!isDiscImageFormatDetectable(lower)) {
|
||||
return DiscImageFormat.OTHER
|
||||
}
|
||||
|
||||
val cacheKey = discFormatCacheKey(uri, sizeBytes)
|
||||
seenKeys.add(cacheKey)
|
||||
|
||||
synchronized(discFormatCacheLock) {
|
||||
discFormatCache[cacheKey]?.let { cached ->
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
val detected = detectDiscImageFormat(uri, fileName)
|
||||
synchronized(discFormatCacheLock) {
|
||||
if (discFormatCache[cacheKey] != detected) {
|
||||
discFormatCache[cacheKey] = detected
|
||||
discFormatCacheDirty = true
|
||||
}
|
||||
}
|
||||
return detected
|
||||
}
|
||||
|
||||
private fun detectDiscImageFormat(uri: Uri, fileName: String): DiscImageFormat {
|
||||
val lower = fileName.lowercase(Locale.ROOT)
|
||||
if (!isDiscImageFormatDetectable(lower)) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import java.io.File
|
||||
|
||||
@@ -15,6 +14,7 @@ class LauncherActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
DebugLog.initialize(this)
|
||||
OrientationLocker(this).enable()
|
||||
|
||||
val prefs = getSharedPreferences("x1box_prefs", MODE_PRIVATE)
|
||||
@@ -113,16 +113,16 @@ class LauncherActivity : Activity() {
|
||||
.commit()
|
||||
|
||||
if (hasMcpx && hasFlash && hasHdd) {
|
||||
Log.i(TAG, "Frontend launch resolved via ${frontendLaunch.source}")
|
||||
DebugLog.i(TAG) { "Frontend launch resolved via ${frontendLaunch.source}" }
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Frontend launch queued, but core setup is incomplete")
|
||||
DebugLog.i(TAG) { "Frontend launch queued, but core setup is incomplete" }
|
||||
Toast.makeText(this, R.string.frontend_launch_setup_required, Toast.LENGTH_SHORT).show()
|
||||
} else if (hasExternalLaunchPayload(intent)) {
|
||||
Log.w(TAG, "Frontend intent received but no accessible game target was resolved")
|
||||
DebugLog.w(TAG) { "Frontend intent received but no accessible game target was resolved" }
|
||||
Toast.makeText(this, R.string.frontend_launch_unresolved, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
@@ -78,6 +77,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
DebugLog.initialize(this)
|
||||
OrientationLocker(this, landscapeOnly = true).enable()
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
val requestedSlot = intent?.getIntExtra(EXTRA_AUTO_LOAD_SNAPSHOT_SLOT, 0) ?: 0
|
||||
@@ -265,9 +265,9 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
0, // nhats
|
||||
0 // nballs
|
||||
)
|
||||
android.util.Log.d("MainActivity", "Virtual controller registered successfully")
|
||||
DebugLog.d("MainActivity") { "Virtual controller registered successfully" }
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to register virtual controller: ${e.message}")
|
||||
DebugLog.e("MainActivity", e) { "Failed to register virtual controller: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.i(TAG, "onDestroy()")
|
||||
DebugLog.i(TAG) { "onDestroy()" }
|
||||
swipeUpGestureRecognizer.reset()
|
||||
inGameMenuDialog?.dismiss()
|
||||
inGameMenuDialog = null
|
||||
@@ -343,7 +343,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
try {
|
||||
org.libsdl.app.SDLControllerManager.nativeRemoveJoystick(-2)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to unregister virtual controller: ${e.message}")
|
||||
DebugLog.e("MainActivity", e) { "Failed to unregister virtual controller: ${e.message}" }
|
||||
}
|
||||
|
||||
inputManager?.unregisterInputDeviceListener(this)
|
||||
@@ -352,17 +352,17 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
Log.i(TAG, "onUserLeaveHint()")
|
||||
DebugLog.i(TAG) { "onUserLeaveHint()" }
|
||||
super.onUserLeaveHint()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.i(TAG, "onTrimMemory(level=$level)")
|
||||
DebugLog.i(TAG) { "onTrimMemory(level=$level)" }
|
||||
super.onTrimMemory(level)
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.w(TAG, "onLowMemory()")
|
||||
DebugLog.w(TAG) { "onLowMemory()" }
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
@@ -803,6 +803,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
private fun exitToGameLibrary() {
|
||||
val intent = Intent(this, GameLibraryActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
putExtra(GameLibraryActivity.EXTRA_INITIAL_ORIENTATION, requestedOrientation)
|
||||
}
|
||||
startActivity(intent)
|
||||
terminateXemuProcessSoon("exit to library")
|
||||
@@ -826,7 +827,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
} catch (_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
Log.i(TAG, "Terminating :xemu process after $reason")
|
||||
DebugLog.i(TAG) { "Terminating :xemu process after $reason" }
|
||||
Process.killProcess(Process.myPid())
|
||||
}.start()
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import java.util.zip.ZipInputStream
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
const val EXTRA_INITIAL_ORIENTATION =
|
||||
"com.izzy2lost.x1box.extra.INITIAL_ORIENTATION"
|
||||
private const val PREF_ADVANCED_EXPERIMENTAL_EXPANDED = "settings_advanced_experimental_expanded"
|
||||
private const val PREF_INSIGNIA_SETUP_URI = "setting_insignia_setup_assistant_uri"
|
||||
private const val PREF_INSIGNIA_SETUP_NAME = "setting_insignia_setup_assistant_name"
|
||||
@@ -129,6 +131,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
private var isImportingDashboard = false
|
||||
private var isPreparingInsignia = false
|
||||
|
||||
private lateinit var switchDebugLogs: MaterialSwitch
|
||||
private lateinit var switchNetworkEnable: MaterialSwitch
|
||||
private lateinit var tvVulkanDriverName: TextView
|
||||
private lateinit var tvInsigniaStatus: TextView
|
||||
@@ -205,9 +208,17 @@ class SettingsActivity : AppCompatActivity() {
|
||||
launchInsigniaSetupAssistant(uri)
|
||||
}
|
||||
|
||||
private val exportDebugLogDocument =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri: Uri? ->
|
||||
uri ?: return@registerForActivityResult
|
||||
exportDebugLog(uri)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
applyInitialOrientationFromIntent()
|
||||
OrientationLocker(this).enable()
|
||||
DebugLog.initialize(this)
|
||||
setContentView(R.layout.activity_settings)
|
||||
EdgeToEdgeHelper.enable(this)
|
||||
EdgeToEdgeHelper.applySystemBarPadding(findViewById(R.id.settings_scroll))
|
||||
@@ -230,10 +241,12 @@ class SettingsActivity : AppCompatActivity() {
|
||||
val switchFpu = findViewById<MaterialSwitch>(R.id.switch_hard_fpu)
|
||||
val switchVsync = findViewById<MaterialSwitch>(R.id.switch_vsync)
|
||||
val switchSkipBootAnim = findViewById<MaterialSwitch>(R.id.switch_skip_boot_anim)
|
||||
switchDebugLogs = findViewById(R.id.switch_debug_logs)
|
||||
val toggleAudioDriver = findViewById<MaterialButtonToggleGroup>(R.id.toggle_audio_driver)
|
||||
val btnSave = findViewById<MaterialButton>(R.id.btn_settings_save)
|
||||
val btnRedoSetup = findViewById<MaterialButton>(R.id.btn_redo_setup_wizard)
|
||||
val btnClearCache = findViewById<MaterialButton>(R.id.btn_clear_system_cache)
|
||||
val btnExportDebugLog = findViewById<MaterialButton>(R.id.btn_export_debug_log)
|
||||
val btnInitializeRetailHdd = findViewById<MaterialButton>(R.id.btn_initialize_retail_hdd)
|
||||
switchNetworkEnable = findViewById(R.id.switch_network_enable)
|
||||
tvInsigniaStatus = findViewById(R.id.tv_insignia_status)
|
||||
@@ -317,9 +330,11 @@ class SettingsActivity : AppCompatActivity() {
|
||||
switchHrtf.isChecked = prefs.getBoolean("setting_hrtf", true)
|
||||
switchShaders.isChecked = prefs.getBoolean("setting_cache_shaders", true)
|
||||
switchFpu.isChecked = prefs.getBoolean("setting_hard_fpu", true)
|
||||
switchVsync.isChecked = prefs.getBoolean("setting_vsync", true)
|
||||
switchVsync.isChecked = prefs.getBoolean("setting_vsync", false)
|
||||
switchSkipBootAnim.isChecked =
|
||||
prefs.getBoolean("setting_skip_boot_anim", false)
|
||||
switchDebugLogs.isChecked =
|
||||
prefs.getBoolean(DebugLog.PREF_ENABLED, false)
|
||||
switchNetworkEnable.isChecked =
|
||||
prefs.getBoolean("setting_network_enable", false)
|
||||
|
||||
@@ -409,6 +424,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
R.id.btn_filtering_nearest -> "nearest"
|
||||
else -> "linear"
|
||||
}
|
||||
val wasDebugLoggingEnabled = prefs.getBoolean(DebugLog.PREF_ENABLED, false)
|
||||
val enableDebugLogs = switchDebugLogs.isChecked
|
||||
|
||||
val edit = prefs.edit()
|
||||
.putInt("setting_display_mode", selectedDisplayMode)
|
||||
@@ -422,6 +439,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
.putBoolean("setting_hard_fpu", switchFpu.isChecked)
|
||||
.putBoolean("setting_vsync", switchVsync.isChecked)
|
||||
.putBoolean("setting_skip_boot_anim", switchSkipBootAnim.isChecked)
|
||||
.putBoolean(DebugLog.PREF_ENABLED, enableDebugLogs)
|
||||
.putBoolean("setting_network_enable", switchNetworkEnable.isChecked)
|
||||
.putString("setting_audio_driver", selectedAudioDriver)
|
||||
.putString("setting_filtering", selectedFiltering)
|
||||
@@ -437,6 +455,11 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
edit.apply()
|
||||
DebugLog.setEnabled(
|
||||
context = this@SettingsActivity,
|
||||
value = enableDebugLogs,
|
||||
resetLogs = enableDebugLogs && !wasDebugLoggingEnabled
|
||||
)
|
||||
|
||||
return applyEepromEdits()
|
||||
}
|
||||
@@ -444,6 +467,13 @@ class SettingsActivity : AppCompatActivity() {
|
||||
btnClearCache.setOnClickListener {
|
||||
showClearCacheConfirmation()
|
||||
}
|
||||
btnExportDebugLog.setOnClickListener {
|
||||
if (!DebugLog.hasAnyLog(this)) {
|
||||
Toast.makeText(this, R.string.settings_export_debug_log_empty, Toast.LENGTH_LONG).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
exportDebugLogDocument.launch(DebugLog.exportDefaultFileName())
|
||||
}
|
||||
|
||||
btnInitializeRetailHdd.setOnClickListener {
|
||||
showInitializeHddLayoutPicker(btnInitializeRetailHdd)
|
||||
@@ -456,6 +486,15 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyInitialOrientationFromIntent() {
|
||||
val initialOrientation = intent.getIntExtra(EXTRA_INITIAL_ORIENTATION, Int.MIN_VALUE)
|
||||
if (initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
|
||||
initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
) {
|
||||
requestedOrientation = initialOrientation
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAdvancedExperimentalExpanded(expanded: Boolean) {
|
||||
layoutAdvancedExperimentalContent.visibility = if (expanded) View.VISIBLE else View.GONE
|
||||
btnToggleAdvancedExperimental.text = getString(
|
||||
@@ -468,6 +507,21 @@ class SettingsActivity : AppCompatActivity() {
|
||||
prefs.edit().putBoolean(PREF_ADVANCED_EXPERIMENTAL_EXPANDED, expanded).apply()
|
||||
}
|
||||
|
||||
private fun exportDebugLog(uri: Uri) {
|
||||
try {
|
||||
contentResolver.openOutputStream(uri, "w")?.use { stream ->
|
||||
DebugLog.exportCombined(this, stream)
|
||||
} ?: throw IOException("Could not open the selected export location.")
|
||||
Toast.makeText(this, R.string.settings_export_debug_log_success, Toast.LENGTH_LONG).show()
|
||||
} catch (error: Exception) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(R.string.settings_export_debug_log_failed, error.message ?: "unknown error"),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openExternalLink(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
@@ -152,6 +151,7 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
DebugLog.initialize(this)
|
||||
OrientationLocker(this).enable()
|
||||
|
||||
mcpxPath = loadValidatedLocalPath("mcpxPath", "mcpxUri", ::isSavedMcpxFileValid)
|
||||
@@ -391,7 +391,7 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
val base = getExternalFilesDir(null) ?: filesDir
|
||||
val dir = File(base, "x1box")
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
Log.e("xemu-android", "Failed to create ${dir.absolutePath}")
|
||||
DebugLog.e("xemu-android") { "Failed to create ${dir.absolutePath}" }
|
||||
return null
|
||||
}
|
||||
val target = File(dir, destName)
|
||||
@@ -403,7 +403,7 @@ class SetupWizardActivity : AppCompatActivity() {
|
||||
} ?: return null
|
||||
target.absolutePath
|
||||
} catch (e: IOException) {
|
||||
Log.e("xemu-android", "Copy failed for $destName", e)
|
||||
DebugLog.e("xemu-android", e) { "Copy failed for $destName" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,6 +798,30 @@
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/xemu_green_light" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_debug_logs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings_debug_logs_enable"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:text="@string/settings_debug_logs_hint"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_export_debug_log"
|
||||
style="@style/Widget.Xemu.Button.Outlined.Pill"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/settings_export_debug_log_action" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_clear_system_cache"
|
||||
style="@style/Widget.Xemu.Button.Outlined.Pill"
|
||||
|
||||
@@ -268,6 +268,12 @@
|
||||
<string name="settings_dashboard_import_boot_alias_note">Boot compatibility: this import did not include C:\\xboxdash.xbe, so %1$s will also be copied as xboxdash.xbe.</string>
|
||||
<string name="settings_dashboard_import_boot_missing_note">Warning: no obvious dashboard XBE was found anywhere under C:. Boot Dashboard may still show the service screen.</string>
|
||||
<string name="settings_section_maintenance">Maintenance</string>
|
||||
<string name="settings_debug_logs_enable">Enable Debug Logs</string>
|
||||
<string name="settings_debug_logs_hint">Writes extra UI and emulator bootstrap logs only while enabled. Turn this on before reproducing a problem, then export the log.</string>
|
||||
<string name="settings_export_debug_log_action">Export Debug Log</string>
|
||||
<string name="settings_export_debug_log_empty">No debug log has been captured yet.</string>
|
||||
<string name="settings_export_debug_log_success">Debug log exported</string>
|
||||
<string name="settings_export_debug_log_failed">Failed to export debug log: %1$s</string>
|
||||
<string name="settings_clear_cache_title">Clear Cache</string>
|
||||
<string name="settings_clear_cache_message">Clear emulator cache files now? This removes shader and app cache data, but keeps your games, saves, and HDD image.</string>
|
||||
<string name="settings_clear_cache_action">Clear Cache</string>
|
||||
|
||||
@@ -183,6 +183,10 @@ static bool sdl2_gl_has_extension(const char *ext_list, const char *ext)
|
||||
|
||||
static void android_log_gl_error(const char *stage)
|
||||
{
|
||||
if (!android_render_logs_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GLenum err;
|
||||
bool logged = false;
|
||||
while ((err = glGetError()) != GL_NO_ERROR) {
|
||||
@@ -1377,7 +1381,7 @@ void xemu_android_display_loop(void)
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
xemu_android_refresh_frame_limit_from_env();
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
SDL_GL_SetSwapInterval(g_config.display.window.vsync ? 1 : 0);
|
||||
xemu_hud_init(m_window, m_context);
|
||||
#endif
|
||||
tcg_register_init_ctx();
|
||||
|
||||
Reference in New Issue
Block a user