Add Android SDL2 scaffold and setup wizard

This commit is contained in:
izzy2lost
2026-01-29 20:29:45 -05:00
parent 26fcbe54f1
commit bcf5651331
33 changed files with 6730 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Android/Gradle
.gradle/
build/
local.properties
*.iml
/.idea/
# Android app outputs
app/build/
app/.cxx/
# OS
.DS_Store
Thumbs.db
+32
View File
@@ -0,0 +1,32 @@
# xemu Android bootstrap
This directory mirrors Super3's Android setup (AGP, NDK, SDK levels) and wires
up SDL2 for an Android-native entry point. It currently builds a minimal SDL2
bootstrap (see `app/src/main/cpp/xemu_android.cpp`) that opens an ES context and
runs an event loop. The xemu core is not yet wired.
## Toolchain expectations
- Android SDK 36
- Build Tools 36.1.0
- NDK r29+ (configured to 29.0.14206865 in Gradle)
- CMake 3.30.3
- JDK 21
## Build
From this directory:
```
./gradlew assembleDebug
```
## SDL2
SDL2 is fetched via CMake (default `release-2.32.10`). To use a local checkout:
```
./gradlew assembleDebug -Pandroid.experimental.cmake.arguments=-DSDL2_LOCAL_DIR=/path/to/SDL
```
## Core integration note
Mainline xemu moved to SDL3 on 2026-01-21. The last SDL2-based tag is `v0.8.133`.
If you plan to wire the core into Android with SDL2, start from that tag or
cherry-pick the SDL2-based frontend changes.
+104
View File
@@ -0,0 +1,104 @@
import java.util.Properties
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
val hasKeystoreProperties = keystorePropertiesFile.exists()
if (hasKeystoreProperties) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
val hasReleaseKeystore = hasKeystoreProperties &&
listOf("storeFile", "storePassword", "keyAlias", "keyPassword").all {
!keystoreProperties.getProperty(it).isNullOrBlank()
}
android {
namespace = "com.izzy2lost.x1box"
compileSdk = 36
buildToolsVersion = "36.1.0"
ndkVersion = "29.0.14206865"
defaultConfig {
applicationId = "com.izzy2lost.x1box"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
ndk {
abiFilters += listOf("arm64-v8a")
}
externalNativeBuild {
cmake {
cppFlags += listOf("-std=c++17", "-fexceptions", "-frtti")
}
}
}
signingConfigs {
if (hasReleaseKeystore) {
create("release") {
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
}
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
if (hasReleaseKeystore) {
signingConfig = signingConfigs.getByName("release")
}
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.30.3"
}
}
packaging {
resources.excludes += setOf(
"**/*.md",
"META-INF/LICENSE*",
"META-INF/NOTICE*"
)
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
}
dependencies {
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.android.material:material:1.14.0-alpha07")
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
View File
+30
View File
@@ -0,0 +1,30 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="@string/app_name"
android:icon="@android:drawable/sym_def_app_icon"
android:allowBackup="true"
android:supportsRtl="true"
android:theme="@style/Theme.Xemu">
<activity
android:name=".MainActivity"
android:screenOrientation="fullSensor"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="SDL_MAIN_LIBRARY"
android:value="xemu" />
</activity>
<activity
android:name=".SetupWizardActivity"
android:screenOrientation="fullSensor"
android:exported="false" />
</application>
</manifest>
+46
View File
@@ -0,0 +1,46 @@
cmake_minimum_required(VERSION 3.30)
project(xemu_android LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# --- SDL2 ---
option(XEMU_FETCH_SDL2 "Download and build SDL2 for Android" ON)
set(SDL2_VERSION "2.32.10")
if(DEFINED SDL2_LOCAL_DIR)
message(STATUS "Using local SDL2 at ${SDL2_LOCAL_DIR}")
add_subdirectory("${SDL2_LOCAL_DIR}" "${CMAKE_BINARY_DIR}/sdl2-local")
elseif(XEMU_FETCH_SDL2)
FetchContent_Declare(
SDL2
URL "https://github.com/libsdl-org/SDL/archive/refs/tags/release-${SDL2_VERSION}.zip"
)
FetchContent_MakeAvailable(SDL2)
endif()
add_library(xemu SHARED
xemu_android.cpp
)
target_compile_definitions(xemu PRIVATE ANDROID __ANDROID__)
target_compile_options(xemu PRIVATE -fexceptions -frtti)
find_library(log-lib log)
find_library(android-lib android)
find_library(egl-lib EGL)
find_library(glesv3-lib GLESv3)
if(TARGET SDL2::SDL2)
target_link_libraries(xemu PRIVATE SDL2::SDL2)
endif()
target_link_libraries(xemu PRIVATE
${log-lib}
${android-lib}
${egl-lib}
${glesv3-lib}
)
+98
View File
@@ -0,0 +1,98 @@
#include <SDL.h>
#include <SDL_main.h>
#include <SDL_system.h>
#include <GLES3/gl3.h>
#include <android/log.h>
namespace {
constexpr const char* kLogTag = "xemu-android";
}
extern "C" int xemu_android_main(int argc, char** argv) __attribute__((weak));
static void LogInfo(const char* msg) {
__android_log_print(ANDROID_LOG_INFO, kLogTag, "%s", msg);
}
extern "C" int SDL_main(int argc, char* argv[]) {
if (xemu_android_main) {
return xemu_android_main(argc, argv);
}
(void)argc;
(void)argv;
SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
SDL_DisableScreenSaver();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER) != 0) {
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "SDL_Init failed: %s", SDL_GetError());
return 1;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_Window* window = SDL_CreateWindow(
"xemu (Android bootstrap)",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
1280,
720,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN
);
if (!window) {
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "SDL_CreateWindow failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_GLContext gl = SDL_GL_CreateContext(window);
if (!gl) {
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "SDL_GL_CreateContext failed: %s", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
SDL_GL_MakeCurrent(window, gl);
SDL_GL_SetSwapInterval(1);
LogInfo("xemu Android bootstrap running (core not wired yet)");
bool running = true;
while (running) {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT) {
running = false;
} else if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_AC_BACK) {
running = false;
}
}
int w = 0;
int h = 0;
SDL_GL_GetDrawableSize(window, &w, &h);
if (w <= 0) w = 1;
if (h <= 0) h = 1;
glViewport(0, 0, w, h);
glClearColor(0.05f, 0.07f, 0.09f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
SDL_GL_SwapWindow(window);
}
SDL_GL_DeleteContext(gl);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
@@ -0,0 +1,28 @@
package com.izzy2lost.x1box
import android.content.Intent
import android.os.Bundle
import org.libsdl.app.SDLActivity
class MainActivity : SDLActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val prefs = getSharedPreferences("x1box_prefs", MODE_PRIVATE)
val setupComplete = prefs.getBoolean("setup_complete", false)
val hasMcpx = prefs.getString("mcpxUri", null) != null
val hasFlash = prefs.getString("flashUri", null) != null
val hasHdd = prefs.getString("hddUri", null) != null
if (!setupComplete || !hasMcpx || !hasFlash || !hasHdd) {
startActivity(Intent(this, SetupWizardActivity::class.java))
finish()
return
}
super.onCreate(savedInstanceState)
}
override fun getLibraries(): Array<String> = arrayOf(
"SDL2",
"xemu",
)
}
@@ -0,0 +1,233 @@
package com.izzy2lost.x1box
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
class SetupWizardActivity : AppCompatActivity() {
private val prefs by lazy { getSharedPreferences("x1box_prefs", MODE_PRIVATE) }
private lateinit var pageMcpx: View
private lateinit var pageFlash: View
private lateinit var pageHdd: View
private lateinit var mcpxPathText: TextView
private lateinit var flashPathText: TextView
private lateinit var hddPathText: TextView
private lateinit var btnBack: MaterialButton
private lateinit var btnNext: MaterialButton
private lateinit var indicatorMcpx: View
private lateinit var indicatorFlash: View
private lateinit var indicatorHdd: View
private var mcpxUri: Uri? = null
private var flashUri: Uri? = null
private var hddUri: Uri? = null
private var currentStep = 0
private val mcpxExts = setOf("bin", "rom", "img")
private val flashExts = setOf("bin", "rom", "img")
private val hddExts = setOf("qcow2", "img")
private val pickMcpx =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
if (!isAllowedExtension(uri, mcpxExts)) {
showExtensionError(mcpxExts)
return@registerForActivityResult
}
persistUriPermission(uri)
mcpxUri = uri
prefs.edit().putString("mcpxUri", uri.toString()).apply()
updateMcpxSelection()
updateButtons()
}
}
private val pickFlash =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
if (!isAllowedExtension(uri, flashExts)) {
showExtensionError(flashExts)
return@registerForActivityResult
}
persistUriPermission(uri)
flashUri = uri
prefs.edit().putString("flashUri", uri.toString()).apply()
updateFlashSelection()
updateButtons()
}
}
private val pickHdd =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
if (!isAllowedExtension(uri, hddExts)) {
showExtensionError(hddExts)
return@registerForActivityResult
}
persistUriPermission(uri)
hddUri = uri
prefs.edit().putString("hddUri", uri.toString()).apply()
updateHddSelection()
updateButtons()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mcpxUri = prefs.getString("mcpxUri", null)?.let(Uri::parse)
flashUri = prefs.getString("flashUri", null)?.let(Uri::parse)
hddUri = prefs.getString("hddUri", null)?.let(Uri::parse)
if (prefs.getBoolean("setup_complete", false) && mcpxUri != null && flashUri != null && hddUri != null) {
goToMain()
return
}
setContentView(R.layout.activity_setup_wizard)
val setupRoot: View = findViewById(R.id.setup_root)
val setupCard: View = findViewById(R.id.setup_card)
setupRoot.post {
val target = (setupRoot.height * 0.92f).toInt()
if (target > 0) {
setupCard.minimumHeight = target
}
}
pageMcpx = findViewById(R.id.page_mcpx)
pageFlash = findViewById(R.id.page_flash)
pageHdd = findViewById(R.id.page_hdd)
mcpxPathText = findViewById(R.id.mcpx_path_text)
flashPathText = findViewById(R.id.flash_path_text)
hddPathText = findViewById(R.id.hdd_path_text)
btnBack = findViewById(R.id.btn_wizard_back)
btnNext = findViewById(R.id.btn_wizard_next)
indicatorMcpx = findViewById(R.id.step_indicator_mcpx)
indicatorFlash = findViewById(R.id.step_indicator_flash)
indicatorHdd = findViewById(R.id.step_indicator_hdd)
val btnPickMcpx: MaterialButton = findViewById(R.id.btn_pick_mcpx)
val btnPickFlash: MaterialButton = findViewById(R.id.btn_pick_flash)
val btnPickHdd: MaterialButton = findViewById(R.id.btn_pick_hdd)
btnPickMcpx.setOnClickListener { pickMcpx.launch(arrayOf("application/octet-stream")) }
btnPickFlash.setOnClickListener { pickFlash.launch(arrayOf("application/octet-stream")) }
btnPickHdd.setOnClickListener { pickHdd.launch(arrayOf("application/x-qcow2", "application/octet-stream")) }
btnBack.setOnClickListener { showStep(currentStep - 1) }
btnNext.setOnClickListener {
if (currentStep < 2) {
showStep(currentStep + 1)
} else {
finishSetup()
}
}
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (currentStep > 0) {
showStep(currentStep - 1)
} else {
finish()
}
}
}
)
updateMcpxSelection()
updateFlashSelection()
updateHddSelection()
showStep(0)
}
private fun showStep(step: Int) {
currentStep = step.coerceIn(0, 2)
pageMcpx.visibility = if (currentStep == 0) View.VISIBLE else View.GONE
pageFlash.visibility = if (currentStep == 1) View.VISIBLE else View.GONE
pageHdd.visibility = if (currentStep == 2) View.VISIBLE else View.GONE
indicatorMcpx.setBackgroundResource(
if (currentStep == 0) R.drawable.setup_wizard_indicator_active else R.drawable.setup_wizard_indicator_inactive
)
indicatorFlash.setBackgroundResource(
if (currentStep == 1) R.drawable.setup_wizard_indicator_active else R.drawable.setup_wizard_indicator_inactive
)
indicatorHdd.setBackgroundResource(
if (currentStep == 2) R.drawable.setup_wizard_indicator_active else R.drawable.setup_wizard_indicator_inactive
)
updateButtons()
}
private fun updateButtons() {
btnBack.visibility = if (currentStep == 0) View.INVISIBLE else View.VISIBLE
btnNext.text = getString(if (currentStep == 2) R.string.setup_finish else R.string.setup_next)
btnNext.isEnabled =
when (currentStep) {
0 -> mcpxUri != null
1 -> flashUri != null
else -> hddUri != null
}
}
private fun updateMcpxSelection() {
val value = mcpxUri?.toString() ?: getString(R.string.setup_not_set)
mcpxPathText.text = getString(R.string.setup_mcpx_value, value)
}
private fun updateFlashSelection() {
val value = flashUri?.toString() ?: getString(R.string.setup_not_set)
flashPathText.text = getString(R.string.setup_flash_value, value)
}
private fun updateHddSelection() {
val value = hddUri?.toString() ?: getString(R.string.setup_not_set)
hddPathText.text = getString(R.string.setup_hdd_value, value)
}
private fun finishSetup() {
prefs.edit().putBoolean("setup_complete", true).apply()
goToMain()
}
private fun goToMain() {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private fun persistUriPermission(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
try {
contentResolver.takePersistableUriPermission(uri, flags)
} catch (_: SecurityException) {
}
}
private fun isAllowedExtension(uri: Uri, allowed: Set<String>): Boolean {
val name = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
if (nameIndex >= 0 && cursor.moveToFirst()) cursor.getString(nameIndex) else null
} ?: uri.lastPathSegment
val ext = name?.substringAfterLast('.', "")?.lowercase().orEmpty()
if (ext.isEmpty()) return false
return allowed.contains(ext)
}
private fun showExtensionError(allowed: Set<String>) {
val pretty = allowed.sorted().joinToString(separator = ", ") { ".$it" }
Toast.makeText(this, "Please pick a file with one of: $pretty", Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,22 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int sendFeatureReport(byte[] report);
public int sendOutputReport(byte[] report);
public boolean getFeatureReport(byte[] report);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,309 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
}
public String getIdentifier() {
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getManufacturerName();
}
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getProductName();
}
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
public String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present
if (mInputEndpoint == null || mOutputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
}
@Override
public int sendOutputReport(byte[] report) {
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (r != report.length) {
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
}
return r;
}
@Override
public boolean getFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceFeatureReport(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
mConnection.close();
mConnection = null;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}
@@ -0,0 +1,90 @@
package org.libsdl.app;
import android.content.Context;
import java.lang.Class;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
public static void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
public static void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
public static void setContext(Context context) {
SDLAudioManager.setContext(context);
mContext = context;
}
public static Context getContext() {
return mContext;
}
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
public static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Context mContext;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,405 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
public boolean mIsSurfaceReady;
// Startup
public SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
public void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
public void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
public Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
try
{
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
}
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip in MultiWindow.
if (skip) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
Log.v("SDL", "Don't skip in Multi-Window");
skip = false;
}
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerFingerId;
int i = -1;
float x,y,p;
/*
* Prevent id to be -1, since it's used in SDL internal for synthetic events
* Appears when using Android emulator, eg:
* adb shell input mouse tap 100 100
* adb shell input touchscreen tap 100 100
*/
if (touchDevId < 0) {
touchDevId -= 1;
}
// 12290 = Samsung DeX mode desktop mouse
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
// 0x2 = SOURCE_CLASS_POINTER
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
int mouseButton = 1;
try {
Object object = event.getClass().getMethod("getButtonState").invoke(event);
if (object != null) {
mouseButton = (Integer) object;
}
} catch(Exception ignored) {
}
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event);
y = motionListener.getEventY(event);
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
} else {
switch(action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
// Primary pointer up/down, the index is always zero
i = 0;
/* fallthrough */
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
// Non primary pointer up/down
if (i == -1) {
i = event.getActionIndex();
}
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
break;
case MotionEvent.ACTION_CANCEL:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
}
break;
default:
break;
}
}
return true;
}
// Sensor events
public void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME, null);
} else {
mSensorManager.unregisterListener(this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newOrientation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
break;
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
break;
}
if (newOrientation != SDLActivity.mCurrentOrientation) {
SDLActivity.mCurrentOrientation = newOrientation;
SDLActivity.onNativeOrientationChanged(newOrientation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Captured pointer events for API 26.
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(0);
y = event.getY(0);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
return false;
}
}
@@ -0,0 +1,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="135"
android:startColor="@color/xemu_black"
android:centerColor="@color/xemu_surface"
android:endColor="@color/xemu_surface_variant" />
</shape>
@@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/xemu_green" />
<corners android:radius="999dp" />
</shape>

Some files were not shown because too many files have changed in this diff Show More