mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-07 12:03:24 -04:00
Many mobile fixes, Android update check, "Background Input" & "Pause On Focus Lost" options
This commit is contained in:
+5
-6
@@ -109,12 +109,7 @@ add_subdirectory(libs/freeverb)
|
||||
option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)")
|
||||
option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.")
|
||||
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
|
||||
if (ANDROID)
|
||||
set(DUSK_ENABLE_UPDATE_CHECKER_DEFAULT OFF)
|
||||
else ()
|
||||
set(DUSK_ENABLE_UPDATE_CHECKER_DEFAULT ON)
|
||||
endif ()
|
||||
option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ${DUSK_ENABLE_UPDATE_CHECKER_DEFAULT})
|
||||
option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
|
||||
|
||||
if(ANDROID)
|
||||
set(DUSK_MOVIE_SUPPORT OFF)
|
||||
@@ -328,6 +323,10 @@ if (DUSK_ENABLE_UPDATE_CHECKER)
|
||||
list(APPEND GAME_LIBS winhttp)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1)
|
||||
message(STATUS "dusk: Enabled update checker (WinHTTP)")
|
||||
elseif (ANDROID)
|
||||
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1)
|
||||
message(STATUS "dusk: Enabled update checker (Android)")
|
||||
elseif (APPLE)
|
||||
find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
|
||||
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm)
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 17be93f0ae...1eeff98783
@@ -11,7 +11,6 @@ namespace dusk {
|
||||
extern bool IsRunning;
|
||||
extern bool IsShuttingDown;
|
||||
extern bool IsGameLaunched;
|
||||
extern bool IsFocusPaused;
|
||||
extern bool RestartRequested;
|
||||
extern std::filesystem::path ConfigPath;
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ struct UserSettings {
|
||||
ConfigVar<float> freeCameraSensitivity;
|
||||
ConfigVar<bool> debugFlyCam;
|
||||
ConfigVar<bool> debugFlyCamLockEvents;
|
||||
ConfigVar<bool> allowBackgroundInput;
|
||||
|
||||
// Cheats
|
||||
ConfigVar<bool> infiniteHearts;
|
||||
|
||||
@@ -2,6 +2,16 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile
|
||||
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
|
||||
from(new File(duskRepoDir, 'res')) {
|
||||
into 'res'
|
||||
exclude '**/.DS_Store'
|
||||
}
|
||||
into duskGeneratedAssetsDir
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.twilitrealm.dusk'
|
||||
compileSdk 36
|
||||
@@ -27,7 +37,7 @@ android {
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
assets.srcDirs = ['../../assets']
|
||||
assets.srcDirs = [duskGeneratedAssetsDir]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,3 +58,9 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.startsWith('merge') && task.name.endsWith('Assets')) {
|
||||
task.dependsOn(syncDuskAssets)
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -1,2 +1,4 @@
|
||||
# Keep SDL activity and related JNI bridge methods.
|
||||
-keep class org.libsdl.app.** { *; }
|
||||
-keep class com.twilitrealm.dusk.DuskHttpClient { *; }
|
||||
-keep class com.twilitrealm.dusk.DuskHttpClient$Response { *; }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<uses-feature android:name="android.hardware.type.pc" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
package com.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
|
||||
private static String[] splitArgs(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
@@ -63,7 +72,10 @@ public class DuskActivity extends SDLActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
|
||||
WindowInsetsController ctrl = getWindow().getDecorView().getWindowInsetsController();
|
||||
if (ctrl != null) {
|
||||
ctrl.hide(WindowInsets.Type.systemBars());
|
||||
}
|
||||
}else {
|
||||
View decorView = getWindow().getDecorView();
|
||||
// Hide the status bar.
|
||||
@@ -72,7 +84,9 @@ public class DuskActivity extends SDLActivity {
|
||||
// Remember that you should never show the action bar if the
|
||||
// status bar is hidden, so hide that too if necessary.
|
||||
ActionBar actionBar = getActionBar();
|
||||
actionBar.hide();
|
||||
if (actionBar != null) {
|
||||
actionBar.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,4 +122,93 @@ public class DuskActivity extends SDLActivity {
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
persistUriPermissions(data);
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void persistUriPermissions(Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int permissionFlags =
|
||||
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (permissionFlags == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
persistUriPermission(uri, permissionFlags);
|
||||
}
|
||||
|
||||
ClipData clipData = data.getClipData();
|
||||
if (clipData == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < clipData.getItemCount(); ++i) {
|
||||
Uri itemUri = clipData.getItemAt(i).getUri();
|
||||
if (itemUri != null) {
|
||||
persistUriPermission(itemUri, permissionFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistUriPermission(Uri uri, int permissionFlags) {
|
||||
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
|
||||
}
|
||||
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
|
||||
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
|
||||
}
|
||||
}
|
||||
|
||||
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
|
||||
try {
|
||||
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayNameForUri(String uriString) {
|
||||
if (uriString == null || uriString.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try (Cursor cursor = getContentResolver().query(
|
||||
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (displayNameColumn >= 0) {
|
||||
String displayName = cursor.getString(displayNameColumn);
|
||||
if (displayName != null && !displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to query display name for " + uri, e);
|
||||
}
|
||||
} else if ("file".equals(uri.getScheme())) {
|
||||
String path = uri.getPath();
|
||||
if (path != null && !path.isEmpty()) {
|
||||
String name = new File(path).getName();
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String lastSegment = uri.getLastPathSegment();
|
||||
return lastSegment != null ? lastSegment : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.twilitrealm.dusk;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
public final class DuskHttpClient {
|
||||
public static final int ERROR_NONE = 0;
|
||||
public static final int ERROR_INVALID_URL = 1;
|
||||
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
|
||||
public static final int ERROR_TIMEOUT = 3;
|
||||
public static final int ERROR_TOO_LARGE = 4;
|
||||
public static final int ERROR_NETWORK = 5;
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
|
||||
public static final class Response {
|
||||
public int error;
|
||||
public String message;
|
||||
public int statusCode;
|
||||
public String[] headerNames;
|
||||
public String[] headerValues;
|
||||
public byte[] body;
|
||||
|
||||
Response(int error, String message, int statusCode, String[] headerNames,
|
||||
String[] headerValues, byte[] body) {
|
||||
this.error = error;
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
this.headerNames = headerNames != null ? headerNames : new String[0];
|
||||
this.headerValues = headerValues != null ? headerValues : new String[0];
|
||||
this.body = body != null ? body : new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private DuskHttpClient() {
|
||||
}
|
||||
|
||||
public static Response get(String url, String[] headerNames, String[] headerValues,
|
||||
int timeoutMs, long maxBodyBytes) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return fail(ERROR_INVALID_URL, "URL is empty");
|
||||
}
|
||||
|
||||
try {
|
||||
URL currentUrl = new URL(url);
|
||||
if (!isHttps(currentUrl)) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
|
||||
}
|
||||
|
||||
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
|
||||
HttpsURLConnection connection =
|
||||
(HttpsURLConnection) currentUrl.openConnection();
|
||||
try {
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(timeoutMs);
|
||||
connection.setReadTimeout(timeoutMs);
|
||||
connection.setUseCaches(false);
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
applyHeaders(connection, headerNames, headerValues);
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if (isRedirect(statusCode)) {
|
||||
String location = connection.getHeaderField("Location");
|
||||
if (location == null || location.isEmpty()) {
|
||||
return fail(ERROR_NETWORK, "Redirect response did not include Location",
|
||||
statusCode, connection, new byte[0]);
|
||||
}
|
||||
|
||||
URL nextUrl = new URL(currentUrl, location);
|
||||
if (!isHttps(nextUrl)) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME,
|
||||
"Only https:// redirects are supported", statusCode,
|
||||
connection, new byte[0]);
|
||||
}
|
||||
currentUrl = nextUrl;
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] body = readBody(connection, statusCode, maxBodyBytes);
|
||||
return success(statusCode, connection, body);
|
||||
} catch (ResponseTooLargeException e) {
|
||||
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
|
||||
safeStatusCode(connection), connection, e.partialBody);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return fail(ERROR_NETWORK, "Too many redirects");
|
||||
} catch (MalformedURLException e) {
|
||||
return fail(ERROR_INVALID_URL, "Failed to parse URL");
|
||||
} catch (SocketTimeoutException e) {
|
||||
return fail(ERROR_TIMEOUT, "Request timed out");
|
||||
} catch (IOException e) {
|
||||
String message = e.getMessage();
|
||||
return fail(ERROR_NETWORK, message != null ? message : e.toString());
|
||||
} catch (ClassCastException e) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyHeaders(HttpsURLConnection connection, String[] names,
|
||||
String[] values) {
|
||||
if (names == null || values == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = Math.min(names.length, values.length);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (names[i] != null && values[i] != null) {
|
||||
connection.setRequestProperty(names[i], values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHttps(URL url) {
|
||||
return "https".equalsIgnoreCase(url.getProtocol());
|
||||
}
|
||||
|
||||
private static boolean isRedirect(int statusCode) {
|
||||
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
|
||||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
|
||||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
|
||||
statusCode == 307 ||
|
||||
statusCode == 308;
|
||||
}
|
||||
|
||||
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
|
||||
long maxBodyBytes) throws IOException,
|
||||
ResponseTooLargeException {
|
||||
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
|
||||
connection.getErrorStream() : connection.getInputStream();
|
||||
if (stream == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
try (InputStream bodyStream = stream;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
long total = 0;
|
||||
while (true) {
|
||||
int read = bodyStream.read(buffer);
|
||||
if (read < 0) {
|
||||
return out.toByteArray();
|
||||
}
|
||||
if (read == 0) {
|
||||
continue;
|
||||
}
|
||||
if (read > maxBodyBytes || total > maxBodyBytes - read) {
|
||||
throw new ResponseTooLargeException(out.toByteArray());
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
total += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int safeStatusCode(HttpsURLConnection connection) {
|
||||
try {
|
||||
return connection.getResponseCode();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
|
||||
HeaderLists headers = readHeaders(connection);
|
||||
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
|
||||
}
|
||||
|
||||
private static Response fail(int error, String message) {
|
||||
return new Response(error, message, 0, null, null, null);
|
||||
}
|
||||
|
||||
private static Response fail(int error, String message, int statusCode,
|
||||
HttpsURLConnection connection, byte[] body) {
|
||||
HeaderLists headers = readHeaders(connection);
|
||||
return new Response(error, message, statusCode, headers.names, headers.values, body);
|
||||
}
|
||||
|
||||
private static HeaderLists readHeaders(HttpsURLConnection connection) {
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> values = new ArrayList<>();
|
||||
|
||||
Map<String, List<String>> headerFields = connection.getHeaderFields();
|
||||
if (headerFields == null) {
|
||||
return new HeaderLists(new String[0], new String[0]);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
List<String> entryValues = entry.getValue();
|
||||
if (entryValues == null || entryValues.isEmpty()) {
|
||||
names.add(name);
|
||||
values.add("");
|
||||
continue;
|
||||
}
|
||||
for (String value : entryValues) {
|
||||
names.add(name);
|
||||
values.add(value != null ? value : "");
|
||||
}
|
||||
}
|
||||
|
||||
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
|
||||
}
|
||||
|
||||
private static final class HeaderLists {
|
||||
final String[] names;
|
||||
final String[] values;
|
||||
|
||||
HeaderLists(String[] names, String[] values) {
|
||||
this.names = names;
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ResponseTooLargeException extends Exception {
|
||||
final byte[] partialBody;
|
||||
|
||||
ResponseTooLargeException(byte[] partialBody) {
|
||||
this.partialBody = partialBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,7 @@ public class HIDDeviceManager {
|
||||
0x24c6, // PowerA
|
||||
0x2c22, // Qanba
|
||||
0x2dc8, // 8BitDo
|
||||
0x37d7, // Flydigi
|
||||
0x9886, // ASTRO Gaming
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
private static final String TAG = "SDL";
|
||||
private static final int SDL_MAJOR_VERSION = 3;
|
||||
private static final int SDL_MINOR_VERSION = 4;
|
||||
private static final int SDL_MICRO_VERSION = 2;
|
||||
private static final int SDL_MICRO_VERSION = 4;
|
||||
/*
|
||||
// Display InputType.SOURCE/CLASS of events and devices
|
||||
//
|
||||
@@ -2032,7 +2032,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
||||
try {
|
||||
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode);
|
||||
return pfd != null ? pfd.detachFd() : -1;
|
||||
} catch (FileNotFoundException e) {
|
||||
} catch (FileNotFoundException | SecurityException e) {
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
}
|
||||
@@ -2227,4 +2227,3 @@ class SDLClipboardHandler implements
|
||||
SDLActivity.onNativeClipboardChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,17 +65,15 @@ class SDLInputConnection extends BaseInputConnection
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
while (beforeLength-- > 0) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
while (beforeLength-- > 0) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
|
||||
return false;
|
||||
|
||||
Regular → Executable
+35
-16
@@ -14,9 +14,22 @@ if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
|
||||
fi
|
||||
|
||||
if [[ -n "$ANDROID_NDK_VER" ]]; then
|
||||
TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then
|
||||
STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip"
|
||||
case "$(uname -s)" in
|
||||
Darwin) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux) HOST_TAG="linux-x86_64" ;;
|
||||
*) HOST_TAG="" ;;
|
||||
esac
|
||||
|
||||
PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt"
|
||||
if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then
|
||||
STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip"
|
||||
else
|
||||
for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
STRIP_TOOL="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -25,29 +38,35 @@ copy_lib() {
|
||||
local src="$2"
|
||||
local dst_dir="$APP_DIR/$abi"
|
||||
local dst="$dst_dir/libmain.so"
|
||||
local tmp="$dst_dir/.libmain.so.$$"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "Missing native library for $abi: $src" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$dst_dir"
|
||||
cp -f "$src" "$dst"
|
||||
cp -f "$src" "$tmp"
|
||||
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
|
||||
"$STRIP_TOOL" --strip-debug "$dst"
|
||||
echo "Staged and stripped $src -> $dst"
|
||||
"$STRIP_TOOL" --strip-unneeded "$tmp"
|
||||
mv -f "$tmp" "$dst"
|
||||
echo "Stripped and staged $src -> $dst"
|
||||
else
|
||||
mv -f "$tmp" "$dst"
|
||||
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
|
||||
fi
|
||||
}
|
||||
|
||||
declare -A ABI_TO_LIB=(
|
||||
["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so"
|
||||
["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so"
|
||||
)
|
||||
|
||||
# Drop any previously staged ABI directories to avoid stale APK contents.
|
||||
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
|
||||
|
||||
for abi in $ANDROID_STAGE_ABIS; do
|
||||
src="${ABI_TO_LIB[$abi]:-}"
|
||||
if [[ -z "$src" ]]; then
|
||||
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$abi" in
|
||||
arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;;
|
||||
x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;;
|
||||
*)
|
||||
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
copy_lib "$abi" "$src"
|
||||
done
|
||||
|
||||
+27
-12
@@ -163,30 +163,30 @@ icon {
|
||||
}
|
||||
|
||||
icon.arrow-forward {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
font-size: 1.2em;
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.trophy {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
font-size: 1.2em;
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.controller {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
font-size: 1.2em;
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.warning {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
font-size: 1.2em;
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
@@ -274,3 +274,18 @@ logo img.outer {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
toast {
|
||||
top: 20dp;
|
||||
right: 20dp;
|
||||
}
|
||||
|
||||
toast.controller-warning {
|
||||
bottom: 20dp;
|
||||
}
|
||||
|
||||
toast.menu-notification {
|
||||
top: 20dp;
|
||||
}
|
||||
}
|
||||
|
||||
+60
-9
@@ -275,7 +275,6 @@ body.mirrored version-info {
|
||||
|
||||
.update {
|
||||
display: none;
|
||||
font-size: 16dp;
|
||||
color: #A6A09B;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
@@ -375,8 +374,8 @@ body.animate-in .intro-item {
|
||||
}
|
||||
|
||||
menu {
|
||||
left: 20dp;
|
||||
right: 20dp;
|
||||
left: 32dp;
|
||||
right: 32dp;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
@@ -387,8 +386,8 @@ body.animate-in .intro-item {
|
||||
}
|
||||
|
||||
body.mirrored menu {
|
||||
left: 20dp;
|
||||
right: 20dp;
|
||||
left: 32dp;
|
||||
right: 32dp;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
@@ -396,7 +395,7 @@ body.animate-in .intro-item {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 48%;
|
||||
|
||||
margin-left: 32dp;
|
||||
}
|
||||
|
||||
body.mirrored hero {
|
||||
@@ -438,9 +437,61 @@ body.animate-in .intro-item {
|
||||
decorator: horizontal-gradient(#FEE685FF #FEE68500);
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
disc-info,
|
||||
version-info {
|
||||
.eyebrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
disc-info {
|
||||
right: 32dp;
|
||||
left: auto;
|
||||
bottom: 32dp;
|
||||
top: auto;
|
||||
text-align: right;
|
||||
font-size: 16dp;
|
||||
}
|
||||
|
||||
#disc-status {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#disc-status icon {
|
||||
font-size: 20dp;
|
||||
}
|
||||
|
||||
#disc-version {
|
||||
font-size: 16dp;
|
||||
}
|
||||
|
||||
version-info {
|
||||
right: 32dp;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
top: 32dp;
|
||||
text-align: right;
|
||||
font-size: 16dp;
|
||||
}
|
||||
|
||||
.update {
|
||||
font-size: 16dp;
|
||||
}
|
||||
|
||||
body.mirrored disc-info {
|
||||
right: auto;
|
||||
left: 32dp;
|
||||
bottom: 32dp;
|
||||
top: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body.mirrored version-info {
|
||||
right: auto;
|
||||
left: 32dp;
|
||||
bottom: auto;
|
||||
top: 32dp;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body.mirrored #disc-status {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
#include "file_select.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
@@ -19,6 +26,92 @@
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
std::string fallback_display_name(std::string_view path) {
|
||||
if (path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string pathString(path);
|
||||
const std::size_t slash = pathString.find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
|
||||
return pathString;
|
||||
}
|
||||
return pathString.substr(slash + 1);
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
bool clear_pending_exception(JNIEnv* env) {
|
||||
if (env == nullptr || !env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string(JNIEnv* env, jstring value) {
|
||||
if (env == nullptr || value == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* utf8 = env->GetStringUTFChars(value, nullptr);
|
||||
if (utf8 == nullptr) {
|
||||
clear_pending_exception(env);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(utf8);
|
||||
env->ReleaseStringUTFChars(value, utf8);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string android_display_name(std::string_view path) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return {};
|
||||
}
|
||||
|
||||
jmethodID getDisplayName = env->GetMethodID(
|
||||
activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (getDisplayName == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return {};
|
||||
}
|
||||
|
||||
jstring uri = env->NewStringUTF(std::string(path).c_str());
|
||||
if (uri == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto* displayName =
|
||||
static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
|
||||
env->DeleteLocalRef(uri);
|
||||
env->DeleteLocalRef(activity);
|
||||
if (displayName == nullptr || clear_pending_exception(env)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result = to_string(env, displayName);
|
||||
env->DeleteLocalRef(displayName);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
struct IOSDialogCallbackState {
|
||||
FileCallback callback;
|
||||
@@ -88,4 +181,16 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
default_location, allow_many);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string display_name_for_path(std::string_view path) {
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
if (path.starts_with("content:") || path.starts_with("file:")) {
|
||||
std::string displayName = android_display_name(path);
|
||||
if (!displayName.empty()) {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return fallback_display_name(path);
|
||||
}
|
||||
} // namespace dusk
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace dusk {
|
||||
@@ -9,7 +12,9 @@ namespace dusk {
|
||||
using FileCallback = void (*)(void* userdata, const char* path, const char* error);
|
||||
|
||||
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many);
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many);
|
||||
|
||||
std::string display_name_for_path(std::string_view path);
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
constexpr int JavaErrorNone = 0;
|
||||
constexpr int JavaErrorInvalidUrl = 1;
|
||||
constexpr int JavaErrorUnsupportedScheme = 2;
|
||||
constexpr int JavaErrorTimeout = 3;
|
||||
constexpr int JavaErrorTooLarge = 4;
|
||||
|
||||
int timeout_ms(std::chrono::milliseconds timeout) {
|
||||
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
return static_cast<int>(
|
||||
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
|
||||
}
|
||||
|
||||
jlong max_body_bytes(size_t maxBodyBytes) {
|
||||
return static_cast<jlong>(std::min<size_t>(
|
||||
maxBodyBytes, static_cast<size_t>(std::numeric_limits<jlong>::max())));
|
||||
}
|
||||
|
||||
bool clear_pending_exception(JNIEnv* env) {
|
||||
if (env == nullptr || !env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string(JNIEnv* env, jstring value) {
|
||||
if (env == nullptr || value == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* utf8 = env->GetStringUTFChars(value, nullptr);
|
||||
if (utf8 == nullptr) {
|
||||
clear_pending_exception(env);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(utf8);
|
||||
env->ReleaseStringUTFChars(value, utf8);
|
||||
return result;
|
||||
}
|
||||
|
||||
jstring to_jstring(JNIEnv* env, std::string_view value) {
|
||||
if (env == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return env->NewStringUTF(std::string(value).c_str());
|
||||
}
|
||||
|
||||
Error map_java_error(int error) {
|
||||
switch (error) {
|
||||
case JavaErrorNone:
|
||||
return Error::None;
|
||||
case JavaErrorInvalidUrl:
|
||||
return Error::InvalidUrl;
|
||||
case JavaErrorUnsupportedScheme:
|
||||
return Error::UnsupportedScheme;
|
||||
case JavaErrorTimeout:
|
||||
return Error::Timeout;
|
||||
case JavaErrorTooLarge:
|
||||
return Error::TooLarge;
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) {
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jmethodID getClassLoader =
|
||||
env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (getClassLoader == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jobject classLoader = env->CallObjectMethod(activity, getClassLoader);
|
||||
if (classLoader == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
|
||||
if (classLoaderClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(classLoader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jmethodID loadClass = env->GetMethodID(
|
||||
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
env->DeleteLocalRef(classLoaderClass);
|
||||
if (loadClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(classLoader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jstring javaClassName = env->NewStringUTF(className);
|
||||
if (javaClassName == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(classLoader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* loadedClass =
|
||||
static_cast<jclass>(env->CallObjectMethod(classLoader, loadClass, javaClassName));
|
||||
env->DeleteLocalRef(javaClassName);
|
||||
env->DeleteLocalRef(classLoader);
|
||||
if (loadedClass == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return loadedClass;
|
||||
}
|
||||
|
||||
jobjectArray make_string_array(JNIEnv* env, const std::vector<Header>& headers, bool names) {
|
||||
jclass stringClass = env->FindClass("java/lang/String");
|
||||
if (stringClass == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jobjectArray array =
|
||||
env->NewObjectArray(static_cast<jsize>(headers.size()), stringClass, nullptr);
|
||||
env->DeleteLocalRef(stringClass);
|
||||
if (array == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (jsize i = 0; i < static_cast<jsize>(headers.size()); ++i) {
|
||||
const std::string& value = names ? headers[static_cast<size_t>(i)].name :
|
||||
headers[static_cast<size_t>(i)].value;
|
||||
jstring javaValue = to_jstring(env, value);
|
||||
if (javaValue == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(array);
|
||||
return nullptr;
|
||||
}
|
||||
env->SetObjectArrayElement(array, i, javaValue);
|
||||
env->DeleteLocalRef(javaValue);
|
||||
if (clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(array);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
std::vector<Header> read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) {
|
||||
std::vector<Header> headers;
|
||||
if (names == nullptr || values == nullptr) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values));
|
||||
headers.reserve(static_cast<size_t>(count));
|
||||
for (jsize i = 0; i < count; ++i) {
|
||||
auto* name = static_cast<jstring>(env->GetObjectArrayElement(names, i));
|
||||
auto* value = static_cast<jstring>(env->GetObjectArrayElement(values, i));
|
||||
if (clear_pending_exception(env)) {
|
||||
if (name != nullptr) {
|
||||
env->DeleteLocalRef(name);
|
||||
}
|
||||
if (value != nullptr) {
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
headers.clear();
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (name != nullptr) {
|
||||
headers.push_back({
|
||||
.name = to_string(env, name),
|
||||
.value = to_string(env, value),
|
||||
});
|
||||
}
|
||||
|
||||
if (name != nullptr) {
|
||||
env->DeleteLocalRef(name);
|
||||
}
|
||||
if (value != nullptr) {
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
std::string read_body(JNIEnv* env, jbyteArray body) {
|
||||
if (body == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const jsize bodySize = env->GetArrayLength(body);
|
||||
std::string result(static_cast<size_t>(bodySize), '\0');
|
||||
if (bodySize > 0) {
|
||||
env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast<jbyte*>(result.data()));
|
||||
if (clear_pending_exception(env)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Result result_from_response(JNIEnv* env, jobject response) {
|
||||
if (response == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Android HTTP request did not return a response",
|
||||
};
|
||||
}
|
||||
|
||||
jclass responseClass = env->GetObjectClass(response);
|
||||
if (responseClass == nullptr || clear_pending_exception(env)) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to inspect Android HTTP response",
|
||||
};
|
||||
}
|
||||
|
||||
jfieldID errorField = env->GetFieldID(responseClass, "error", "I");
|
||||
jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;");
|
||||
jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I");
|
||||
jfieldID headerNamesField =
|
||||
env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;");
|
||||
jfieldID headerValuesField =
|
||||
env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;");
|
||||
jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B");
|
||||
env->DeleteLocalRef(responseClass);
|
||||
if (errorField == nullptr || messageField == nullptr || statusField == nullptr ||
|
||||
headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr ||
|
||||
clear_pending_exception(env))
|
||||
{
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Android HTTP response shape was not recognized",
|
||||
};
|
||||
}
|
||||
|
||||
const int javaError = env->GetIntField(response, errorField);
|
||||
auto* message = static_cast<jstring>(env->GetObjectField(response, messageField));
|
||||
auto* headerNames = static_cast<jobjectArray>(env->GetObjectField(response, headerNamesField));
|
||||
auto* headerValues =
|
||||
static_cast<jobjectArray>(env->GetObjectField(response, headerValuesField));
|
||||
auto* body = static_cast<jbyteArray>(env->GetObjectField(response, bodyField));
|
||||
if (clear_pending_exception(env)) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to read Android HTTP response",
|
||||
};
|
||||
}
|
||||
|
||||
Response httpResponse{
|
||||
.statusCode = static_cast<int>(env->GetIntField(response, statusField)),
|
||||
.headers = read_headers(env, headerNames, headerValues),
|
||||
.body = read_body(env, body),
|
||||
};
|
||||
|
||||
std::string messageString = to_string(env, message);
|
||||
|
||||
if (message != nullptr) {
|
||||
env->DeleteLocalRef(message);
|
||||
}
|
||||
if (headerNames != nullptr) {
|
||||
env->DeleteLocalRef(headerNames);
|
||||
}
|
||||
if (headerValues != nullptr) {
|
||||
env->DeleteLocalRef(headerValues);
|
||||
}
|
||||
if (body != nullptr) {
|
||||
env->DeleteLocalRef(body);
|
||||
}
|
||||
|
||||
return {
|
||||
.error = map_java_error(javaError),
|
||||
.message = std::move(messageString),
|
||||
.response = std::move(httpResponse),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::Android;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "Android";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
if (!request.url.starts_with("https://")) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to access Android JNI environment",
|
||||
};
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to access Android activity",
|
||||
};
|
||||
}
|
||||
|
||||
jclass clientClass =
|
||||
load_dusk_class(env, activity, "com.twilitrealm.dusk.DuskHttpClient");
|
||||
env->DeleteLocalRef(activity);
|
||||
if (clientClass == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to load Android HTTP helper",
|
||||
};
|
||||
}
|
||||
|
||||
jmethodID getMethod = env->GetStaticMethodID(clientClass, "get",
|
||||
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)"
|
||||
"Lcom/twilitrealm/dusk/DuskHttpClient$Response;");
|
||||
if (getMethod == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(clientClass);
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to find Android HTTP helper method",
|
||||
};
|
||||
}
|
||||
|
||||
jstring url = to_jstring(env, request.url);
|
||||
jobjectArray headerNames = make_string_array(env, request.headers, true);
|
||||
jobjectArray headerValues = make_string_array(env, request.headers, false);
|
||||
if (url == nullptr || headerNames == nullptr || headerValues == nullptr ||
|
||||
clear_pending_exception(env))
|
||||
{
|
||||
if (url != nullptr) {
|
||||
env->DeleteLocalRef(url);
|
||||
}
|
||||
if (headerNames != nullptr) {
|
||||
env->DeleteLocalRef(headerNames);
|
||||
}
|
||||
if (headerValues != nullptr) {
|
||||
env->DeleteLocalRef(headerValues);
|
||||
}
|
||||
env->DeleteLocalRef(clientClass);
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to prepare Android HTTP request",
|
||||
};
|
||||
}
|
||||
|
||||
jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames,
|
||||
headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes));
|
||||
env->DeleteLocalRef(url);
|
||||
env->DeleteLocalRef(headerNames);
|
||||
env->DeleteLocalRef(headerValues);
|
||||
env->DeleteLocalRef(clientClass);
|
||||
if (clear_pending_exception(env)) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Android HTTP request failed with a Java exception",
|
||||
};
|
||||
}
|
||||
|
||||
Result result = result_from_response(env, response);
|
||||
if (response != nullptr) {
|
||||
env->DeleteLocalRef(response);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -13,6 +13,7 @@ enum class Backend {
|
||||
WinHttp,
|
||||
UrlSession,
|
||||
LibCurl,
|
||||
Android,
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
|
||||
@@ -82,6 +82,7 @@ UserSettings g_userSettings = {
|
||||
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
|
||||
.debugFlyCam {"game.debugFlyCam", false},
|
||||
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
|
||||
.allowBackgroundInput {"game.allowBackgroundInput", true},
|
||||
|
||||
// Cheats
|
||||
.infiniteHearts {"game.infiniteHearts", false},
|
||||
@@ -215,6 +216,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.freeCamera);
|
||||
Register(g_userSettings.game.debugFlyCam);
|
||||
Register(g_userSettings.game.debugFlyCamLockEvents);
|
||||
Register(g_userSettings.game.allowBackgroundInput);
|
||||
|
||||
Register(g_userSettings.backend.isoPath);
|
||||
Register(g_userSettings.backend.isoVerification);
|
||||
|
||||
@@ -445,8 +445,7 @@ private:
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName =
|
||||
std::filesystem::path(sDiscVerificationTask->path).filename().string();
|
||||
std::string fileName = display_name_for_path(sDiscVerificationTask->path);
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "graphics_tuner.hpp"
|
||||
@@ -317,7 +318,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (path.empty()) {
|
||||
display = "(none)";
|
||||
} else {
|
||||
display = std::filesystem::path(path).filename().string();
|
||||
display = display_name_for_path(path);
|
||||
if (display.empty()) {
|
||||
display = path;
|
||||
}
|
||||
@@ -612,6 +613,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
pane.clear();
|
||||
pane.add_text("Open controller binding configuration.");
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput,
|
||||
{
|
||||
.key = "Allow Background Input",
|
||||
.helpText = "Allow controller input even when the game window is not focused.",
|
||||
.onChange = [](bool value) { aurora_set_background_input(value); },
|
||||
});
|
||||
|
||||
leftPane.add_section("Camera");
|
||||
addOption("Free Camera", getSettings().game.freeCamera,
|
||||
@@ -923,6 +930,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.helpText = "Checks GitHub releases for a new Dusk version on startup.<br/><br/>"
|
||||
"No personal information is transmitted or collected.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
|
||||
{
|
||||
.key = "Pause On Focus Lost",
|
||||
.helpText = "Pause the game when window focus is lost.",
|
||||
.onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings,
|
||||
{
|
||||
.key = "Enable Advanced Settings",
|
||||
|
||||
+1
-5
@@ -255,11 +255,7 @@ void update() noexcept {
|
||||
}
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (basePath == nullptr) {
|
||||
return std::filesystem::path("res") / filename;
|
||||
}
|
||||
return std::filesystem::path(basePath) / "res" / filename;
|
||||
return std::filesystem::path("res") / filename;
|
||||
}
|
||||
|
||||
std::string escape(std::string_view str) noexcept {
|
||||
|
||||
@@ -16,10 +16,7 @@ namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
float base_body_padding(Rml::Context* context) noexcept {
|
||||
if (context == nullptr) {
|
||||
return 64.0f;
|
||||
}
|
||||
const float dpRatio = std::max(context->GetDensityIndependentPixelRatio(), 0.001f);
|
||||
const float dpRatio = context->GetDensityIndependentPixelRatio();
|
||||
const float heightDp = static_cast<float>(context->GetDimensions().y) / dpRatio;
|
||||
if (heightDp <= 640.0f) {
|
||||
return 16.0f * dpRatio;
|
||||
|
||||
+31
-19
@@ -105,7 +105,6 @@ const int audioHeapSize = 0x14D800;
|
||||
bool dusk::IsRunning = true;
|
||||
bool dusk::IsShuttingDown = false;
|
||||
bool dusk::IsGameLaunched = false;
|
||||
bool dusk::IsFocusPaused = false;
|
||||
bool dusk::RestartRequested = false;
|
||||
std::filesystem::path dusk::ConfigPath;
|
||||
#endif
|
||||
@@ -228,19 +227,16 @@ void main01(void) {
|
||||
switch (event->type) {
|
||||
case AURORA_NONE:
|
||||
goto eventsDone;
|
||||
case AURORA_PAUSED:
|
||||
dusk::audio::SetPaused(true);
|
||||
break;
|
||||
case AURORA_UNPAUSED:
|
||||
dusk::audio::SetPaused(false);
|
||||
dusk::game_clock::reset_frame_timer();
|
||||
break;
|
||||
case AURORA_SDL_EVENT:
|
||||
dusk::ui::handle_event(event->sdl);
|
||||
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
|
||||
if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST &&
|
||||
dusk::getSettings().game.pauseOnFocusLost) {
|
||||
dusk::IsFocusPaused = true;
|
||||
dusk::audio::SetPaused(true);
|
||||
} else if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED &&
|
||||
dusk::IsFocusPaused) {
|
||||
dusk::IsFocusPaused = false;
|
||||
dusk::audio::SetPaused(false);
|
||||
dusk::game_clock::reset_frame_timer();
|
||||
}
|
||||
break;
|
||||
case AURORA_DISPLAY_SCALE_CHANGED:
|
||||
dusk::ImGuiEngine_Initialize(event->windowSize.scale);
|
||||
@@ -254,19 +250,14 @@ void main01(void) {
|
||||
|
||||
eventsDone:;
|
||||
|
||||
if (dusk::IsFocusPaused) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||
if (!aurora_begin_frame()) {
|
||||
DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame");
|
||||
continue;
|
||||
}
|
||||
|
||||
VIWaitForRetrace();
|
||||
|
||||
dusk::lastFrameAuroraStats = *aurora_get_stats();
|
||||
if (!aurora_begin_frame()) {
|
||||
DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame");
|
||||
continue;
|
||||
}
|
||||
|
||||
mDoGph_gInf_c::updateRenderSize();
|
||||
|
||||
dusk::ui::update();
|
||||
@@ -571,7 +562,8 @@ int game_main(int argc, char* argv[]) {
|
||||
config.logLevel = startupLogLevel;
|
||||
config.mem1Size = 256 * 1024 * 1024;
|
||||
config.mem2Size = 24 * 1024 * 1024;
|
||||
config.allowJoystickBackgroundEvents = true;
|
||||
config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput;
|
||||
config.pauseOnFocusLost = dusk::getSettings().game.pauseOnFocusLost;
|
||||
config.imGuiInitCallback = &aurora_imgui_init_callback;
|
||||
config.allowTextureReplacements = true;
|
||||
config.allowTextureDumps = false;
|
||||
@@ -618,13 +610,19 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up.
|
||||
// This is only a metadata check; full hash verification is handled by the prelaunch UI.
|
||||
bool forcePreLaunchUI = false;
|
||||
bool saveConfigBeforePrelaunch = false;
|
||||
|
||||
const std::string p = dusk::getSettings().backend.isoPath;
|
||||
dusk::iso::DiscInfo discInfo{};
|
||||
if (!p.empty() &&
|
||||
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p);
|
||||
dusk::getSettings().backend.isoPath.setValue("");
|
||||
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
|
||||
forcePreLaunchUI = true;
|
||||
saveConfigBeforePrelaunch = true;
|
||||
}
|
||||
|
||||
std::string dvd_path;
|
||||
@@ -636,6 +634,7 @@ int game_main(int argc, char* argv[]) {
|
||||
dvd_opened = aurora_dvd_open(dvd_path.c_str());
|
||||
if (!dvd_opened) {
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
|
||||
forcePreLaunchUI = true;
|
||||
} else {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::getSettings().backend.isoVerification.setValue(
|
||||
@@ -645,10 +644,23 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
} else {
|
||||
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
|
||||
forcePreLaunchUI = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dvd_opened) {
|
||||
if (dusk::getSettings().backend.isoPath.getValue().empty()) {
|
||||
forcePreLaunchUI = true;
|
||||
}
|
||||
if (forcePreLaunchUI && dusk::getSettings().backend.skipPreLaunchUI.getValue()) {
|
||||
DuskLog.warn("Prelaunch UI was disabled with no usable DVD image, enabling prelaunch UI");
|
||||
dusk::getSettings().backend.skipPreLaunchUI.setValue(false);
|
||||
saveConfigBeforePrelaunch = true;
|
||||
}
|
||||
if (saveConfigBeforePrelaunch) {
|
||||
dusk::config::Save();
|
||||
}
|
||||
|
||||
if (!dusk::getSettings().backend.skipPreLaunchUI) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user