diff --git a/Knossos.NET.Android/Properties/AndroidManifest.xml b/Knossos.NET.Android/Properties/AndroidManifest.xml index a4306270..34b17e64 100644 --- a/Knossos.NET.Android/Properties/AndroidManifest.xml +++ b/Knossos.NET.Android/Properties/AndroidManifest.xml @@ -1,4 +1,4 @@ - + @@ -39,6 +39,7 @@ - + shortEdges + + - + + + diff --git a/Knossos.NET.Android/java/com/knossosnet/knossosnet/GameActivity.java b/Knossos.NET.Android/java/com/knossosnet/knossosnet/GameActivity.java index e9765ce7..feb7e4a2 100644 --- a/Knossos.NET.Android/java/com/knossosnet/knossosnet/GameActivity.java +++ b/Knossos.NET.Android/java/com/knossosnet/knossosnet/GameActivity.java @@ -90,14 +90,26 @@ protected String getMainFunction() { } private void hideSystemUI() { - View decorView = getWindow().getDecorView(); - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + Window window = getWindow(); + View decorView = window.getDecorView(); + + if (Build.VERSION.SDK_INT >= 30) { + window.setDecorFitsSystemWindows(false); + WindowInsetsController c = window.getInsetsController(); + if (c != null) { + c.hide(WindowInsets.Type.systemBars()); + c.setSystemBarsBehavior( + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } else { + decorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } } @Override @@ -140,6 +152,12 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } catch (Throwable ignored) { } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + { // API 28+ + getWindow().getAttributes().layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } hideSystemUI(); @@ -162,8 +180,8 @@ protected void onCreate(Bundle savedInstanceState) { } private static final String[] PREFERRED_ORDER = new String[] { - "libSDL2.so", - "libshaderc.so", + "libSDL3.so", + "libshaderc_shared.so", "libopenal.so", "libavutil.so", "libswresample.so", @@ -215,6 +233,10 @@ private static List orderForLoad(File dir) { private boolean tryLoad(File so) { try { + String name = so.getName(); + if (name.equals("libSDL2.so")) { + return true; + } if (so != null && so.isFile()) { System.load(so.getAbsolutePath()); return true; diff --git a/Knossos.NET.Android/java/org/libsdl/app/HIDDevice.java b/Knossos.NET.Android/java/org/libsdl/app/HIDDevice.java index 955df5d1..f9609532 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/HIDDevice.java +++ b/Knossos.NET.Android/java/org/libsdl/app/HIDDevice.java @@ -13,9 +13,8 @@ interface HIDDevice 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 int writeReport(byte[] report, boolean feature); + public boolean readReport(byte[] report, boolean feature); public void setFrozen(boolean frozen); public void close(); public void shutdown(); diff --git a/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceBLESteamController.java b/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceBLESteamController.java index ee5521fd..bcd8806c 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ b/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -19,9 +19,13 @@ import java.lang.Runnable; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.UUID; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { private static final String TAG = "hidapi"; @@ -33,10 +37,19 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private boolean mIsConnected = false; private boolean mIsChromebook = false; private boolean mIsReconnecting = false; + private boolean mHasEnabledNotifications = false; + private boolean mHasSeenInputUpdate = false; private boolean mFrozen = false; private LinkedList mOperations; GattOperation mCurrentOperation = null; private Handler mHandler; + private int mProductId = -1; + private int mReportId = 0; + private UUID mInputCharacteristic; + + private static final int D0G_BLE2_PID = 0x1106; + private static final int TRITON_BLE_PID = 0x1303; + private static final int TRANSPORT_AUTO = 0; private static final int TRANSPORT_BREDR = 1; @@ -44,11 +57,15 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; - static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); - static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); + static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3"); + static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + private HashMap mOutputReportChars = new HashMap(); + static class GattOperation { private enum Operation { CHR_READ, @@ -61,6 +78,7 @@ private enum Operation { byte[] mValue; BluetoothGatt mGatt; boolean mResult = true; + int mDelayMs = 0; private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { mGatt = gatt; @@ -68,6 +86,13 @@ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUI mUuid = uuid; } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mDelayMs = delayMs; + } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { mGatt = gatt; mOp = operation; @@ -75,6 +100,14 @@ private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUI mValue = value; } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mValue = value; + mDelayMs = delayMs; + } + public void run() { // This is executed in main thread BluetoothGattCharacteristic chr; @@ -136,6 +169,8 @@ public boolean finish() { return mResult; } + public int getDelayMs() { return mDelayMs; } + private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { BluetoothGattService valveService = mGatt.getService(steamControllerService); if (valveService == null) @@ -154,32 +189,38 @@ static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, b static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); } + + static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) { + return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs); + } } - public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { + HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { mManager = manager; mDevice = device; mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); mIsRegistered = false; - mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + mIsChromebook = SDLActivity.isChromebook(); mOperations = new LinkedList(); mHandler = new Handler(Looper.getMainLooper()); mGatt = connectGatt(); + mHasEnabledNotifications = false; + mHasSeenInputUpdate = false; // final HIDDeviceBLESteamController finalThis = this; // mHandler.postDelayed(new Runnable() { // @Override - // public void run() { + // void run() { // finalThis.checkConnectionForChromebookIssue(); // } // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); } - public String getIdentifier() { + String getIdentifier() { return String.format("SteamController.%s", mDevice.getAddress()); } - public BluetoothGatt getGatt() { + BluetoothGatt getGatt() { return mGatt; } @@ -219,7 +260,7 @@ protected int getConnectionState() { return btManager.getConnectionState(mDevice, BluetoothProfile.GATT); } - public void reconnect() { + void reconnect() { if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) { mGatt.disconnect(); @@ -314,8 +355,45 @@ private boolean probeService(HIDDeviceBLESteamController controller) { Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(inputCharacteristic)) { - Log.v(TAG, "Found input characteristic"); + if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) { + Log.v(TAG, "Found Triton input characteristic 0x45"); + mProductId = TRITON_BLE_PID; + mReportId = 0x45; + mInputCharacteristic = chr.getUuid(); + } else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) { + Log.v(TAG, "Found Triton input characteristic 0x47"); + mProductId = TRITON_BLE_PID; + mReportId = 0x47; + mInputCharacteristic = chr.getUuid(); + } else if (chr.getUuid().equals(inputCharacteristicD0G)) { + Log.v(TAG, "Found D0G input characteristic"); + mProductId = D0G_BLE2_PID; + mReportId = 0x03; + mInputCharacteristic = chr.getUuid(); + } else { + Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE); + Matcher matcher = reportPattern.matcher(chr.getUuid().toString()); + + if (matcher.find()) { + try { + int reportId = Integer.parseInt(matcher.group(1), 16); + + reportId -= 0x35; + if (reportId >= 0x80) { + // This is a Triton output report characteristic that we need to care about. + Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16)); + mOutputReportChars.put(reportId, chr); + } + } + catch (NumberFormatException nfe) { + Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString()); + } + } + } + } + + for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { + if (chr.getUuid().equals(mInputCharacteristic)) { // Start notifications BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); if (cccd != null) { @@ -372,21 +450,30 @@ private void executeNextGattOperation() { mCurrentOperation = mOperations.removeFirst(); } - // Run in main thread - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (mOperations) { - if (mCurrentOperation == null) { - Log.e(TAG, "Current operation null in executor?"); - return; - } + Runnable gattOperationRunnable = new Runnable() { + @Override + public void run() { + synchronized (mOperations) { + if (mCurrentOperation == null) { + Log.e(TAG, "Current operation null in executor?"); + return; + } - mCurrentOperation.run(); - // now wait for the GATT callback and when it comes, finish this operation + mCurrentOperation.run(); + // now wait for the GATT callback and when it comes, finish this operation + } } - } - }); + }; + + if (mCurrentOperation.getDelayMs() == 0) { + // Run in main thread + mHandler.post(gattOperationRunnable); + } + else { + // If we have a delay on this operation, wait before we post it. + mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs()); + } + } private void queueGattOperation(GattOperation op) { @@ -397,16 +484,47 @@ private void queueGattOperation(GattOperation op) { } private void enableNotification(UUID chrUuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); + // Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting + // it will return success and then silently drop the operation on the floor. + GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500); queueGattOperation(op); + + // Amazon Fire devices can also silently timeout on writeDescriptor, so + // set up a little delayed check that will attempt to write a second time. + // + // While this only seems to be needed on Amazon Fire TV devices at present, it + // doesn't hurt to have a retry on other devices as well. + // + final HIDDeviceBLESteamController finalThis = this; + final UUID finalUuid = chrUuid; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!finalThis.mHasEnabledNotifications) { + + if (finalThis.mHasSeenInputUpdate) { + // Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen + // input reports, though, somewhat by definition notifications are enabled. + Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization."); + finalThis.mHasEnabledNotifications = true; + finalThis.enableValveMode(); + return; + } + + // Give one more try. + GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500); + finalThis.queueGattOperation(retry); + } + } + }, 1000); } - public void writeCharacteristic(UUID uuid, byte[] value) { + void writeCharacteristic(UUID uuid, byte[] value) { GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); queueGattOperation(op); } - public void readCharacteristic(UUID uuid) { + void readCharacteristic(UUID uuid) { GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); queueGattOperation(op); } @@ -415,6 +533,7 @@ public void readCharacteristic(UUID uuid) { ////////////// BluetoothGattCallback overridden methods ////////////////////////////////////////////////////////////////////////////////////////////////////// + @Override public void onConnectionStateChange(BluetoothGatt g, int status, int newState) { //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState); mIsReconnecting = false; @@ -437,6 +556,7 @@ else if (newState == 0) { // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent. } + @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { //Log.v(TAG, "onServicesDiscovered status=" + status); if (status == 0) { @@ -446,23 +566,33 @@ public void onServicesDiscovered(BluetoothGatt gatt, int status) { mIsConnected = false; gatt.disconnect(); mGatt = connectGatt(false); - } - else { + } else { + if (getProductId() == TRITON_BLE_PID) { + // Android will not properly play well with Data Length Extensions without manually requesting a large MTU, + // and Triton controllers require DLE support. + // + // 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change + // this value. It is functionally "please enable data length extensions" on some Android builds. + mGatt.requestMtu(517); + } + probeService(this); } } } + @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { - mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); + mManager.HIDDeviceReportResponse(getId(), characteristic.getValue()); } finishCurrentGattOperation(); } + @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid()); @@ -470,7 +600,7 @@ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristi // Only register controller with the native side once it has been fully configured if (!isRegistered()) { Log.v(TAG, "Registering Steam Controller with ID: " + getId()); - mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); setRegistered(); } } @@ -478,44 +608,68 @@ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristi finishCurrentGattOperation(); } + @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { // Enable this for verbose logging of controller input reports //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); - if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) { + mHasSeenInputUpdate = true; mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); } } + @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { //Log.v(TAG, "onDescriptorRead status=" + status); } - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); - //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); + private void enableValveMode() + { + BluetoothGattService valveService = mGatt.getService(steamControllerService); + if (valveService == null) + return; - if (chr.getUuid().equals(inputCharacteristic)) { - boolean hasWrittenInputDescriptor = true; - BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); - if (reportChr != null) { + BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic); + if (reportChr != null) { + if (getProductId() == TRITON_BLE_PID) { + // For Triton we just mark things registered. + Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); + setRegistered(); + } else { + // For the original controller, we need to manually enter Valve mode. Log.v(TAG, "Writing report characteristic to enter valve mode"); reportChr.setValue(enterValveMode); - gatt.writeCharacteristic(reportChr); + mGatt.writeCharacteristic(reportChr); } } + } + + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); + //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); + + if (chr.getUuid().equals(mInputCharacteristic)) { + mHasEnabledNotifications = true; + enableValveMode(); + } finishCurrentGattOperation(); } + @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { //Log.v(TAG, "onReliableWriteCompleted status=" + status); } + @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { //Log.v(TAG, "onReadRemoteRssi status=" + status); } + @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { //Log.v(TAG, "onMtuChanged status=" + status); } @@ -538,9 +692,20 @@ public int getVendorId() { @Override public int getProductId() { - // We don't have an easy way to query from the Bluetooth device, but we know what it is - final int D0G_BLE2_PID = 0x1106; - return D0G_BLE2_PID; + if (mProductId > 0) { + // We've already set a product ID. + return mProductId; + } + + if (mDevice.getName().startsWith("Steam Ctrl")) { + // We're a newer Triton device + mProductId = TRITON_BLE_PID; + } else { + // We're an OG Steam Controller + mProductId = D0G_BLE2_PID; + } + + return mProductId; } @Override @@ -575,50 +740,64 @@ public boolean open() { } @Override - public int sendFeatureReport(byte[] report) { + public int writeReport(byte[] report, boolean feature) { if (!isRegistered()) { - Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); + Log.e(TAG, "Attempted writeReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return -1; } - // We need to skip the first byte, as that doesn't go over the air - byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); - writeCharacteristic(reportCharacteristic, actual_report); - return report.length; - } + if (feature) { + // We need to skip the first byte, as that doesn't go over the air + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report)); + writeCharacteristic(reportCharacteristic, actual_report); + return report.length; + } else { + // If we're an original-recipe Steam Controller we just write to the characteristic directly. + if (getProductId() == D0G_BLE2_PID) { + //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } - @Override - public int sendOutputReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); + // If we're a Triton, we need to find the correct report characteristic. + if (report.length > 0) { + int reportId = report[0] & 0xFF; + BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId); + if (targetedReportCharacteristic != null) { + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report)); + writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report); + return report.length; + } else { + Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16)); + } } - return -1; } - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; + return -1; } @Override - public boolean getFeatureReport(byte[] report) { + public boolean readReport(byte[] report, boolean feature) { if (!isRegistered()) { - Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); + Log.e(TAG, "Attempted readReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return false; } - //Log.v(TAG, "getFeatureReport"); - readCharacteristic(reportCharacteristic); - return true; + if (feature) { + readCharacteristic(reportCharacteristic); + return true; + } else { + // Not implemented + return false; + } } @Override diff --git a/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceManager.java b/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceManager.java index 21a1c1d1..691416c1 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceManager.java +++ b/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceManager.java @@ -32,7 +32,7 @@ public class HIDDeviceManager { private static HIDDeviceManager sManager; private static int sManagerRefCount = 0; - public static HIDDeviceManager acquire(Context context) { + static public HIDDeviceManager acquire(Context context) { if (sManagerRefCount == 0) { sManager = new HIDDeviceManager(context); } @@ -40,7 +40,7 @@ public static HIDDeviceManager acquire(Context context) { return sManager; } - public static void release(HIDDeviceManager manager) { + static public void release(HIDDeviceManager manager) { if (manager == sManager) { --sManagerRefCount; if (sManagerRefCount == 0) { @@ -108,12 +108,12 @@ private HIDDeviceManager(final Context context) { HIDDeviceRegisterCallback(); mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); - mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + mIsChromebook = SDLActivity.isChromebook(); // if (shouldClear) { // SharedPreferences.Editor spedit = mSharedPreferences.edit(); // spedit.clear(); -// spedit.commit(); +// spedit.apply(); // } // else { @@ -121,11 +121,11 @@ private HIDDeviceManager(final Context context) { } } - public Context getContext() { + Context getContext() { return mContext; } - public int getDeviceIDForIdentifier(String identifier) { + int getDeviceIDForIdentifier(String identifier) { SharedPreferences.Editor spedit = mSharedPreferences.edit(); int result = mSharedPreferences.getInt(identifier, 0); @@ -135,7 +135,7 @@ public int getDeviceIDForIdentifier(String identifier) { } spedit.putInt(identifier, result); - spedit.commit(); + spedit.apply(); return result; } @@ -193,7 +193,11 @@ private void initializeUSB() { filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); - mContext.registerReceiver(mUsbBroadcast, filter); + if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */ + mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED); + } else { + mContext.registerReceiver(mUsbBroadcast, filter); + } for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { handleUsbDeviceAttached(usbDevice); @@ -252,6 +256,8 @@ private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterfa 0x24c6, // PowerA 0x2c22, // Qanba 0x2dc8, // 8BitDo + 0x3537, // GameSir + 0x37d7, // Flydigi 0x9886, // ASTRO Gaming }; @@ -284,9 +290,13 @@ private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterfa 0x1532, // Razer Wildcat 0x20d6, // PowerA 0x24c6, // PowerA + 0x294b, // Snakebyte 0x2dc8, // 8BitDo 0x2e24, // Hyperkin + 0x2e95, // SCUF + 0x3285, // Nacon 0x3537, // GameSir + 0x366c, // ByoWave }; if (usbInterface.getId() == 0 && @@ -351,7 +361,7 @@ private void connectHIDDeviceUSB(UsbDevice usbDevice) { HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); int id = device.getId(); mDevicesById.put(id, device); - HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol()); + HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0); } } } @@ -372,7 +382,7 @@ private void initializeBluetooth() { return; } - if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); return; } @@ -404,7 +414,11 @@ private void initializeBluetooth() { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); - mContext.registerReceiver(mBluetoothBroadcast, filter); + if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */ + mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED); + } else { + mContext.registerReceiver(mBluetoothBroadcast, filter); + } if (mIsChromebook) { mHandler = new Handler(Looper.getMainLooper()); @@ -431,7 +445,7 @@ private void shutdownBluetooth() { // Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly. // This function provides a sort of dummy version of that, watching for changes in the // connected devices and attempting to add controllers as things change. - public void chromebookConnectionHandler() { + void chromebookConnectionHandler() { if (!mIsChromebook) { return; } @@ -470,7 +484,7 @@ public void run() { }, 10000); } - public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { + boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice); synchronized (this) { if (mBluetoothDevices.containsKey(bluetoothDevice)) { @@ -491,7 +505,7 @@ public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) { return true; } - public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { + void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { synchronized (this) { HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice); if (device == null) @@ -505,7 +519,7 @@ public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) { } } - public boolean isSteamController(BluetoothDevice bluetoothDevice) { + boolean isSteamController(BluetoothDevice bluetoothDevice) { // Sanity check. If you pass in a null device, by definition it is never a Steam Controller. if (bluetoothDevice == null) { return false; @@ -516,7 +530,13 @@ public boolean isSteamController(BluetoothDevice bluetoothDevice) { return false; } - return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + // Steam Controllers will always support Bluetooth Low Energy + if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) { + return false; + } + + // Match on the name either the original Steam Controller or the new second-generation one advertise with. + return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl"); } private void close() { @@ -559,7 +579,7 @@ private HIDDevice getDevice(int id) { ////////// JNI interface functions ////////////////////////////////////////////////////////////////////////////////////////////////////// - public boolean initialize(boolean usb, boolean bluetooth) { + boolean initialize(boolean usb, boolean bluetooth) { Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); if (usb) { @@ -571,7 +591,7 @@ public boolean initialize(boolean usb, boolean bluetooth) { return true; } - public boolean openDevice(int deviceID) { + boolean openDevice(int deviceID) { Log.v(TAG, "openDevice deviceID=" + deviceID); HIDDevice device = getDevice(deviceID); if (device == null) { @@ -591,13 +611,10 @@ public boolean openDevice(int deviceID) { } else { flags = 0; } - if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) { - Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION); - intent.setPackage(mContext.getPackageName()); - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags)); - } else { - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); - } + + Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION); + intent.setPackage(mContext.getPackageName()); + mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags)); } catch (Exception e) { Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); HIDDeviceOpenResult(deviceID, false); @@ -613,26 +630,9 @@ public boolean openDevice(int deviceID) { return false; } - public int sendOutputReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.sendOutputReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - public int sendFeatureReport(int deviceID, byte[] report) { + int writeReport(int deviceID, byte[] report, boolean feature) { try { - //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); + //Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length); HIDDevice device; device = getDevice(deviceID); if (device == null) { @@ -640,16 +640,16 @@ public int sendFeatureReport(int deviceID, byte[] report) { return -1; } - return device.sendFeatureReport(report); + return device.writeReport(report, feature); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } return -1; } - public boolean getFeatureReport(int deviceID, byte[] report) { + boolean readReport(int deviceID, byte[] report, boolean feature) { try { - //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); + //Log.v(TAG, "readReport deviceID=" + deviceID); HIDDevice device; device = getDevice(deviceID); if (device == null) { @@ -657,14 +657,14 @@ public boolean getFeatureReport(int deviceID, byte[] report) { return false; } - return device.getFeatureReport(report); + return device.readReport(report, feature); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } return false; } - public void closeDevice(int deviceID) { + void closeDevice(int deviceID) { try { Log.v(TAG, "closeDevice deviceID=" + deviceID); HIDDevice device; @@ -688,11 +688,11 @@ public void closeDevice(int deviceID) { private native void HIDDeviceRegisterCallback(); private native void HIDDeviceReleaseCallback(); - native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol); + native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID); native void HIDDeviceOpenPending(int deviceID); native void HIDDeviceOpenResult(int deviceID, boolean opened); native void HIDDeviceDisconnected(int deviceID); native void HIDDeviceInputReport(int deviceID, byte[] report); - native void HIDDeviceFeatureReport(int deviceID, byte[] report); + native void HIDDeviceReportResponse(int deviceID, byte[] report); } diff --git a/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceUSB.java b/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceUSB.java index bfe0cf95..08875d2b 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceUSB.java +++ b/Knossos.NET.Android/java/org/libsdl/app/HIDDeviceUSB.java @@ -4,6 +4,7 @@ import android.os.Build; import android.util.Log; import java.util.Arrays; +import java.util.Locale; class HIDDeviceUSB implements HIDDevice { @@ -20,6 +21,7 @@ class HIDDeviceUSB implements HIDDevice { protected InputThread mInputThread; protected boolean mRunning; protected boolean mFrozen; + protected boolean mClaimed; public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { mManager = manager; @@ -28,10 +30,11 @@ public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface mInterface = mDevice.getInterface(mInterfaceIndex).getId(); mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); mRunning = false; + mClaimed = false; } - public String getIdentifier() { - return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); + String getIdentifier() { + return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex); } @Override @@ -52,13 +55,11 @@ public int 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()); - } + try { + result = mDevice.getSerialNumber(); + } + catch (Exception exception) { + //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); } if (result == null) { result = ""; @@ -73,10 +74,8 @@ public int getVersion() { @Override public String getManufacturerName() { - String result = null; - if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { - result = mDevice.getManufacturerName(); - } + String result; + result = mDevice.getManufacturerName(); if (result == null) { result = String.format("%x", getVendorId()); } @@ -85,10 +84,8 @@ public String getManufacturerName() { @Override public String getProductName() { - String result = null; - if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { - result = mDevice.getProductName(); - } + String result; + result = mDevice.getProductName(); if (result == null) { result = String.format("%x", getProductId()); } @@ -100,7 +97,7 @@ public UsbDevice getDevice() { return mDevice; } - public String getDeviceName() { + String getDeviceName() { return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")"; } @@ -119,6 +116,7 @@ public boolean open() { close(); return false; } + mClaimed = true; // Find the endpoints for (int j = 0; j < iface.getEndpointCount(); j++) { @@ -137,9 +135,12 @@ public boolean open() { } } - // Make sure the required endpoints were present - if (mInputEndpoint == null || mOutputEndpoint == null) { + // Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT + // actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall + // back to the Android system gamepad functionality (and lose our paddles et al). + if (mInputEndpoint == null) { Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + mConnection.releaseInterface(iface); close(); return false; } @@ -153,55 +154,80 @@ public boolean open() { } @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; + public int writeReport(byte[] report, boolean feature) { + if (mConnection == null) { + Log.w(TAG, "writeReport() called with no device connection"); + return -1; } - 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()); + if (!mClaimed) { + Log.w(TAG, "writeReport() called but some other process currently owns the USB device"); return -1; } - if (skipped_report_id) { - ++length; - } - return length; - } + if (feature) { + 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; + } - @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()); + 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, "writeFeatureReport() returned " + res + " on device " + getDeviceName()); + return -1; + } + + if (skipped_report_id) { + ++length; + } + return length; + } else { + if (mOutputEndpoint == null) + { + Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!"); + return -1; + } + int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); + if (res != report.length) { + Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName()); + } + return res; } - return r; } @Override - public boolean getFeatureReport(byte[] report) { + public boolean readReport(byte[] report, boolean feature) { int res = -1; int offset = 0; int length = report.length; boolean skipped_report_id = false; byte report_number = report[0]; + if (mConnection == null) { + Log.w(TAG, "readReport() called with no device connection"); + return false; + } + if (!mClaimed) { + if (feature) { + return false; + } + return true; + } + if (report_number == 0x0) { /* Offset the return buffer by 1, so that the report ID will remain in byte 0. */ @@ -213,7 +239,7 @@ public boolean getFeatureReport(byte[] report) { res = mConnection.controlTransfer( UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, 0x01/*HID get_report*/, - (3/*HID feature*/ << 8) | report_number, + ((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number, mInterface, report, offset, length, 1000/*timeout millis*/); @@ -234,7 +260,7 @@ public boolean getFeatureReport(byte[] report) { } else { data = Arrays.copyOfRange(report, 0, res); } - mManager.HIDDeviceFeatureReport(mDeviceId, data); + mManager.HIDDeviceReportResponse(mDeviceId, data); return true; } @@ -254,10 +280,13 @@ public void close() { mInputThread = null; } if (mConnection != null) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - mConnection.releaseInterface(iface); + if (mClaimed) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + mConnection.releaseInterface(iface); + } mConnection.close(); mConnection = null; + mClaimed = false; } } @@ -270,6 +299,22 @@ public void shutdown() { @Override public void setFrozen(boolean frozen) { mFrozen = frozen; + + /* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */ + if (mConnection != null && mClaimed == mFrozen) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + if (frozen) { + mClaimed = !mConnection.releaseInterface(iface); + if (mClaimed) { + Log.e(TAG, "Tried to release claim on USB device, but failed!"); + } + } else { + mClaimed = mConnection.claimInterface(iface, true); + if (!mClaimed) { + Log.e(TAG, "Tried to regain claim on USB device, but failed!"); + } + } + } } protected class InputThread extends Thread { diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDL.java b/Knossos.NET.Android/java/org/libsdl/app/SDL.java index 139be9d1..d9650a72 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDL.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDL.java @@ -1,8 +1,8 @@ package org.libsdl.app; +import android.app.Activity; import android.content.Context; -import java.lang.Class; import java.lang.reflect.Method; /** @@ -12,14 +12,14 @@ 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() { + static public void setupJNI() { SDLActivity.nativeSetupJNI(); SDLAudioManager.nativeSetupJNI(); SDLControllerManager.nativeSetupJNI(); } // This function should be called each time the activity is started - public static void initialize() { + static public void initialize() { setContext(null); SDLActivity.initialize(); @@ -28,33 +28,33 @@ public static void initialize() { } // This function stores the current activity (SDL or not) - public static void setContext(Context context) { + static public void setContext(Activity context) { SDLAudioManager.setContext(context); mContext = context; } - public static Context getContext() { + static public Activity getContext() { return mContext; } - public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { + static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { loadLibrary(libraryName, mContext); } - public static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException { + 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 + // 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 + // 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"); @@ -62,7 +62,7 @@ public static void loadLibrary(String libraryName, Context context) throws Unsat 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 + // 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); @@ -86,5 +86,5 @@ public static void loadLibrary(String libraryName, Context context) throws Unsat } } - protected static Context mContext; + protected static Activity mContext; } diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLActivity.java b/Knossos.NET.Android/java/org/libsdl/app/SDLActivity.java index c1a5422c..4672a0bd 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLActivity.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLActivity.java @@ -4,6 +4,7 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.UiModeManager; +import android.content.ActivityNotFoundException; import android.content.ClipboardManager; import android.content.ClipData; import android.content.Context; @@ -22,10 +23,9 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.LocaleList; import android.os.Message; -import android.text.Editable; -import android.text.InputType; -import android.text.Selection; +import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; @@ -39,17 +39,17 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; import android.widget.Button; -import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.Hashtable; import java.util.Locale; @@ -59,9 +59,9 @@ */ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { private static final String TAG = "SDL"; - private static final int SDL_MAJOR_VERSION = 2; - private static final int SDL_MINOR_VERSION = 33; - private static final int SDL_MICRO_VERSION = 0; + private static final int SDL_MAJOR_VERSION = 3; + private static final int SDL_MINOR_VERSION = 4; + private static final int SDL_MICRO_VERSION = 13; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -107,11 +107,9 @@ public static void debugSource(int sources, String prefix) { if ((s & tst) == tst) src += " GAMEPAD"; s2 &= ~tst; - if (Build.VERSION.SDK_INT >= 21) { - tst = InputDevice.SOURCE_HDMI; - if ((s & tst) == tst) src += " HDMI"; - s2 &= ~tst; - } + tst = InputDevice.SOURCE_HDMI; + if ((s & tst) == tst) src += " HDMI"; + s2 &= ~tst; tst = InputDevice.SOURCE_JOYSTICK; if ((s & tst) == tst) src += " JOYSTICK"; @@ -146,11 +144,9 @@ public static void debugSource(int sources, String prefix) { if ((s & tst) == tst) src += " TOUCHSCREEN"; s2 &= ~tst; - if (Build.VERSION.SDK_INT >= 18) { - tst = InputDevice.SOURCE_TOUCH_NAVIGATION; - if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; - s2 &= ~tst; - } + tst = InputDevice.SOURCE_TOUCH_NAVIGATION; + if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; + s2 &= ~tst; tst = InputDevice.SOURCE_TRACKBALL; if ((s & tst) == tst) src += " TRACKBALL"; @@ -186,6 +182,14 @@ public static void debugSource(int sources, String prefix) { private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; private static final int SDL_SYSTEM_CURSOR_NO = 10; private static final int SDL_SYSTEM_CURSOR_HAND = 11; + private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT = 12; + private static final int SDL_SYSTEM_CURSOR_WINDOW_TOP = 13; + private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT = 14; + private static final int SDL_SYSTEM_CURSOR_WINDOW_RIGHT = 15; + private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT = 16; + private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOM = 17; + private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT = 18; + private static final int SDL_SYSTEM_CURSOR_WINDOW_LEFT = 19; protected static final int SDL_ORIENTATION_UNKNOWN = 0; protected static final int SDL_ORIENTATION_LANDSCAPE = 1; @@ -193,7 +197,7 @@ public static void debugSource(int sources, String prefix) { protected static final int SDL_ORIENTATION_PORTRAIT = 3; protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; - protected static int mCurrentOrientation; + protected static int mCurrentRotation; protected static Locale mCurrentLocale; // Handle the state of the native layer @@ -210,32 +214,53 @@ public enum NativeState { // Main components protected static SDLActivity mSingleton; protected static SDLSurface mSurface; - protected static DummyEdit mTextEdit; - protected static boolean mScreenKeyboardShown; + protected static SDLDummyEdit mTextEdit; protected static ViewGroup mLayout; protected static SDLClipboardHandler mClipboardHandler; protected static Hashtable mCursors; protected static int mLastCursorID; - protected static SDLGenericMotionListener_API12 mMotionListener; + protected static SDLGenericMotionListener_API14 mMotionListener; protected static HIDDeviceManager mHIDDeviceManager; // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; + protected static boolean mSDLMainFinished = false; + protected static boolean mActivityCreated = false; + private static SDLFileDialogState mFileDialogState = null; + protected static boolean mDispatchingKeyEvent = false; - protected static SDLGenericMotionListener_API12 getMotionListener() { + public static SDLGenericMotionListener_API14 getMotionListener() { if (mMotionListener == null) { - if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { + if (Build.VERSION.SDK_INT >= 29 /* Android 10 (Q) */) { + mMotionListener = new SDLGenericMotionListener_API29(); + } else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { mMotionListener = new SDLGenericMotionListener_API26(); } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { mMotionListener = new SDLGenericMotionListener_API24(); } else { - mMotionListener = new SDLGenericMotionListener_API12(); + mMotionListener = new SDLGenericMotionListener_API14(); } } return mMotionListener; } + /** + * The application entry point, called on a dedicated thread (SDLThread). + * The default implementation uses the getMainSharedObject() and getMainFunction() methods + * to invoke native code from the specified shared library. + * It can be overridden by derived classes. + */ + protected void main() { + String library = SDLActivity.mSingleton.getMainSharedObject(); + String function = SDLActivity.mSingleton.getMainFunction(); + String[] arguments = SDLActivity.mSingleton.getArguments(); + + Log.v("SDL", "Running main function " + function + " from library " + library); + SDLActivity.nativeRunMain(library, function, arguments); + Log.v("SDL", "Finished main function"); + } + /** * This method returns the name of the shared object with the application entry point * It can be overridden by derived classes. @@ -263,17 +288,17 @@ protected String getMainFunction() { * This method is called by SDL before loading the native shared libraries. * It can be overridden to provide names of shared libraries to be loaded. * The default implementation returns the defaults. It never returns null. - * An array returned by a new implementation must at least contain "SDL2". + * An array returned by a new implementation must at least contain "SDL3". * Also keep in mind that the order the libraries are loaded may matter. - * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). */ protected String[] getLibraries() { return new String[] { - "SDL2", - // "SDL2_image", - // "SDL2_mixer", - // "SDL2_net", - // "SDL2_ttf", + "SDL3", + // "SDL3_image", + // "SDL3_mixer", + // "SDL3_net", + // "SDL3_ttf", "main" }; } @@ -311,7 +336,7 @@ public static void initialize() { mNextNativeState = NativeState.INIT; mCurrentNativeState = NativeState.INIT; } - + protected SDLSurface createSDLSurface(Context context) { return new SDLSurface(context); } @@ -319,11 +344,30 @@ protected SDLSurface createSDLSurface(Context context) { // Setup @Override protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "Manufacturer: " + Build.MANUFACTURER); Log.v(TAG, "Device: " + Build.DEVICE); Log.v(TAG, "Model: " + Build.MODEL); Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); + + /* Control activity re-creation */ + if (mSDLMainFinished || mActivityCreated) { + boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); + if (mSDLMainFinished) { + Log.v(TAG, "SDL main() finished"); + } + if (allow_recreate) { + Log.v(TAG, "activity re-created"); + } else { + Log.v(TAG, "activity finished"); + System.exit(0); + return; + } + } + + mActivityCreated = true; + try { Thread.currentThread().setName("SDLActivity"); } catch (Exception e) { @@ -378,6 +422,24 @@ public void onClick(DialogInterface dialog,int id) { return; } + + /* Control activity re-creation */ + /* Robustness: check that the native code is run for the first time. + * (Maybe Activity was reset, but not the native code.) */ + { + int run_count = SDLActivity.nativeCheckSDLThreadCounter(); /* get and increment a native counter */ + if (run_count != 0) { + boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); + if (allow_recreate) { + Log.v(TAG, "activity re-created // run_count: " + run_count); + } else { + Log.v(TAG, "activity finished // run_count: " + run_count); + System.exit(0); + return; + } + } + } + // Set up JNI SDL.setupJNI(); @@ -399,9 +461,9 @@ public void onClick(DialogInterface dialog,int id) { mLayout.addView(mSurface); // Get our current screen orientation and pass it down. - mCurrentOrientation = SDLActivity.getCurrentOrientation(); - // Only record current orientation - SDLActivity.onNativeOrientationChanged(mCurrentOrientation); + SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); + mCurrentRotation = SDLActivity.getCurrentRotation(); + SDLActivity.onNativeRotationChanged(mCurrentRotation); try { if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { @@ -412,6 +474,15 @@ public void onClick(DialogInterface dialog,int id) { } catch(Exception ignored) { } + switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { + case Configuration.UI_MODE_NIGHT_NO: + SDLActivity.onNativeDarkModeChanged(false); + break; + case Configuration.UI_MODE_NIGHT_YES: + SDLActivity.onNativeDarkModeChanged(true); + break; + } + setContentView(mLayout); setWindowStyle(false); @@ -459,7 +530,8 @@ protected void onPause() { if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(true); - } + } + if (!mHasMultiWindow) { pauseNativeThread(); } @@ -472,7 +544,8 @@ protected void onResume() { if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(false); - } + } + if (!mHasMultiWindow) { resumeNativeThread(); } @@ -496,33 +569,47 @@ protected void onStart() { } } - public static int getCurrentOrientation() { + public static int getNaturalOrientation() { int result = SDL_ORIENTATION_UNKNOWN; - Activity activity = (Activity)getContext(); - if (activity == null) { - return result; - } - Display display = activity.getWindowManager().getDefaultDisplay(); - - switch (display.getRotation()) { - case Surface.ROTATION_0: - result = SDL_ORIENTATION_PORTRAIT; - break; - - case Surface.ROTATION_90: + Activity activity = getContext(); + if (activity != null) { + Configuration config = activity.getResources().getConfiguration(); + Display display = activity.getWindowManager().getDefaultDisplay(); + int rotation = display.getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && + config.orientation == Configuration.ORIENTATION_LANDSCAPE) || + ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && + config.orientation == Configuration.ORIENTATION_PORTRAIT)) { result = SDL_ORIENTATION_LANDSCAPE; - break; - - case Surface.ROTATION_180: - result = SDL_ORIENTATION_PORTRAIT_FLIPPED; - break; - - case Surface.ROTATION_270: - result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; - break; + } else { + result = SDL_ORIENTATION_PORTRAIT; + } } + return result; + } + public static int getCurrentRotation() { + int result = 0; + + Activity activity = getContext(); + if (activity != null) { + Display display = activity.getWindowManager().getDefaultDisplay(); + switch (display.getRotation()) { + case Surface.ROTATION_0: + result = 0; + break; + case Surface.ROTATION_90: + result = 90; + break; + case Surface.ROTATION_180: + result = 180; + break; + case Surface.ROTATION_270: + result = 270; + break; + } + } return result; } @@ -531,6 +618,14 @@ public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + // If we are gaining focus, we can always try to restore our USB devices. If we are losing focus, + // only try to relinquish them if we don't have background events allowed (for multi-window Android setups). + if (hasFocus || !SDLActivity.nativeGetHintBoolean("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", false)) { + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(!hasFocus); + } + } + if (SDLActivity.mBrokenLibraries) { return; } @@ -553,9 +648,9 @@ public void onWindowFocusChanged(boolean hasFocus) { } @Override - public void onLowMemory() { - Log.v(TAG, "onLowMemory()"); - super.onLowMemory(); + public void onTrimMemory(int level) { + Log.v(TAG, "onTrimMemory()"); + super.onTrimMemory(level); if (SDLActivity.mBrokenLibraries) { return; @@ -577,6 +672,15 @@ public void onConfigurationChanged(Configuration newConfig) { mCurrentLocale = newConfig.locale; SDLActivity.onNativeLocaleChanged(); } + + switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) { + case Configuration.UI_MODE_NIGHT_NO: + SDLActivity.onNativeDarkModeChanged(false); + break; + case Configuration.UI_MODE_NIGHT_YES: + SDLActivity.onNativeDarkModeChanged(true); + break; + } } @Override @@ -602,7 +706,11 @@ protected void onDestroy() { // Wait for "SDLThread" thread to end try { - SDLActivity.mSDLThread.join(); + // Use a timeout because: + // C SDLmain() thread might have started (mSDLThread.start() called) + // while the SDL_Init() might not have been called yet, + // and so the previous QUIT event will be discarded by SDL_Init() and app is running, not exiting. + SDLActivity.mSDLThread.join(1000); } catch(Exception e) { Log.v(TAG, "Problem stopping SDLThread: " + e); } @@ -632,6 +740,43 @@ public void onBackPressed() { } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (mFileDialogState != null && mFileDialogState.requestCode == requestCode) { + /* This is our file dialog */ + String[] filelist = null; + + if (data != null) { + Uri singleFileUri = data.getData(); + + if (singleFileUri == null) { + /* Use Intent.getClipData to get multiple choices */ + ClipData clipData = data.getClipData(); + assert clipData != null; + + filelist = new String[clipData.getItemCount()]; + + for (int i = 0; i < filelist.length; i++) { + String uri = clipData.getItemAt(i).getUri().toString(); + filelist[i] = uri; + } + } else { + /* Only one file is selected. */ + filelist = new String[]{singleFileUri.toString()}; + } + } else { + /* User cancelled the request. */ + filelist = new String[0]; + } + + // TODO: Detect the file MIME type and pass the filter value accordingly. + SDLActivity.onNativeFileDialog(requestCode, filelist, -1); + mFileDialogState = null; + } + } + // Called by JNI from SDL. public static void manualBackButton() { mSingleton.pressBackButton(); @@ -671,7 +816,14 @@ public boolean dispatchKeyEvent(KeyEvent event) { ) { return false; } - return super.dispatchKeyEvent(event); + mDispatchingKeyEvent = true; + boolean result = super.dispatchKeyEvent(event); + mDispatchingKeyEvent = false; + return result; + } + + public static boolean dispatchingKeyEvent() { + return mDispatchingKeyEvent; } /* Transition to next state */ @@ -703,7 +855,7 @@ public static void handleNativeState() { // Try a transition to resumed state if (mNextNativeState == NativeState.RESUMED) { - if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSurface.mIsSurfaceReady && (mHasFocus || mHasMultiWindow) && mIsResumedCalled) { if (mSDLThread == null) { // This is the entry point to the C app. // Start up the C app thread and enable sensor input for the first time @@ -725,11 +877,10 @@ public static void handleNativeState() { } // Messages from the SDLMain thread - static final int COMMAND_CHANGE_TITLE = 1; - static final int COMMAND_CHANGE_WINDOW_STYLE = 2; - static final int COMMAND_TEXTEDIT_HIDE = 3; - static final int COMMAND_SET_KEEP_SCREEN_ON = 5; - + protected static final int COMMAND_CHANGE_TITLE = 1; + protected static final int COMMAND_CHANGE_WINDOW_STYLE = 2; + protected static final int COMMAND_TEXTEDIT_HIDE = 3; + protected static final int COMMAND_SET_KEEP_SCREEN_ON = 5; protected static final int COMMAND_USER = 0x8000; protected static boolean mFullscreenModeActive; @@ -738,6 +889,8 @@ public static void handleNativeState() { * This method is called by SDL if SDL did not handle a message itself. * This happens if a received message contains an unsupported command. * Method can be overwritten to handle Messages in a different class. + * @param command the command of the message. + * @param param the parameter of the message. May be null. * @return if the message was handled in overridden method. */ protected boolean onUnhandledMessage(int command, Object param) { @@ -752,7 +905,7 @@ protected boolean onUnhandledMessage(int command, Object param) { protected static class SDLCommandHandler extends Handler { @Override public void handleMessage(Message msg) { - Context context = SDL.getContext(); + Context context = getContext(); if (context == null) { Log.e(TAG, "error handling message, getContext() returned null"); return; @@ -766,35 +919,37 @@ public void handleMessage(Message msg) { } break; case COMMAND_CHANGE_WINDOW_STYLE: - if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { - if (context instanceof Activity) { - Window window = ((Activity) context).getWindow(); - if (window != null) { - if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { - int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - SDLActivity.mFullscreenModeActive = true; - } else { - int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - SDLActivity.mFullscreenModeActive = false; - } - if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) { - window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; - } + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + SDLActivity.mFullscreenModeActive = true; + } else { + int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; + window.getDecorView().setSystemUiVisibility(flags); + window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + SDLActivity.mFullscreenModeActive = false; + } + if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) { + window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + } + if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */ && + Build.VERSION.SDK_INT < 35 /* Android 15 */) { + SDLActivity.onNativeInsetsChanged(0, 0, 0, 0); } - } else { - Log.e(TAG, "error handling message, getContext() returned no Activity"); } + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); } break; case COMMAND_TEXTEDIT_HIDE: @@ -807,7 +962,7 @@ public void handleMessage(Message msg) { InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); - mScreenKeyboardShown = false; + onNativeScreenKeyboardHidden(); mSurface.requestFocus(); } @@ -838,59 +993,57 @@ public void handleMessage(Message msg) { Handler commandHandler = new SDLCommandHandler(); // Send a message from the SDLMain thread - boolean sendCommand(int command, Object data) { + protected boolean sendCommand(int command, Object data) { Message msg = commandHandler.obtainMessage(); msg.arg1 = command; msg.obj = data; boolean result = commandHandler.sendMessage(msg); - if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { - if (command == COMMAND_CHANGE_WINDOW_STYLE) { - // Ensure we don't return until the resize has actually happened, - // or 500ms have passed. - - boolean bShouldWait = false; - - if (data instanceof Integer) { - // Let's figure out if we're already laid out fullscreen or not. - Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - DisplayMetrics realMetrics = new DisplayMetrics(); - display.getRealMetrics(realMetrics); - - boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && - (realMetrics.heightPixels == mSurface.getHeight())); - - if ((Integer) data == 1) { - // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going - // to change size and should wait for surfaceChanged() before we return, so the size - // is right back in native code. If we're already laid out fullscreen, though, we're - // not going to change size even if we change decor modes, so we shouldn't wait for - // surfaceChanged() -- which may not even happen -- and should return immediately. - bShouldWait = !bFullscreenLayout; - } else { - // If we're laid out fullscreen (even if the status bar and nav bar are present), - // or are actively in fullscreen, we're going to change size and should wait for - // surfaceChanged before we return, so the size is right back in native code. - bShouldWait = bFullscreenLayout; - } + if (command == COMMAND_CHANGE_WINDOW_STYLE) { + // Ensure we don't return until the resize has actually happened, + // or 500ms have passed. + + boolean bShouldWait = false; + + if (data instanceof Integer) { + // Let's figure out if we're already laid out fullscreen or not. + Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + + boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && + (realMetrics.heightPixels == mSurface.getHeight())); + + if ((Integer) data == 1) { + // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going + // to change size and should wait for surfaceChanged() before we return, so the size + // is right back in native code. If we're already laid out fullscreen, though, we're + // not going to change size even if we change decor modes, so we shouldn't wait for + // surfaceChanged() -- which may not even happen -- and should return immediately. + bShouldWait = !bFullscreenLayout; + } else { + // If we're laid out fullscreen (even if the status bar and nav bar are present), + // or are actively in fullscreen, we're going to change size and should wait for + // surfaceChanged before we return, so the size is right back in native code. + bShouldWait = bFullscreenLayout; } + } - if (bShouldWait && (SDLActivity.getContext() != null)) { - // We'll wait for the surfaceChanged() method, which will notify us - // when called. That way, we know our current size is really the - // size we need, instead of grabbing a size that's still got - // the navigation and/or status bars before they're hidden. - // - // We'll wait for up to half a second, because some devices - // take a surprisingly long time for the surface resize, but - // then we'll just give up and return. - // - synchronized (SDLActivity.getContext()) { - try { - SDLActivity.getContext().wait(500); - } catch (InterruptedException ie) { - ie.printStackTrace(); - } + if (bShouldWait && (getContext() != null)) { + // We'll wait for the surfaceChanged() method, which will notify us + // when called. That way, we know our current size is really the + // size we need, instead of grabbing a size that's still got + // the navigation and/or status bars before they're hidden. + // + // We'll wait for up to half a second, because some devices + // take a surprisingly long time for the surface resize, but + // then we'll just give up and return. + // + synchronized (getContext()) { + try { + getContext().wait(500); + } catch (InterruptedException ie) { + ie.printStackTrace(); } } } @@ -901,7 +1054,9 @@ boolean sendCommand(int command, Object data) { // C functions we call public static native String nativeGetVersion(); - public static native int nativeSetupJNI(); + public static native void nativeSetupJNI(); + public static native void nativeInitMainThread(); + public static native void nativeCleanupMainThread(); public static native int nativeRunMain(String library, String function, Object arguments); public static native void nativeLowMemory(); public static native void nativeSendQuit(); @@ -910,7 +1065,7 @@ boolean sendCommand(int command, Object data) { public static native void nativeResume(); public static native void nativeFocusChanged(boolean hasFocus); public static native void onNativeDropFile(String filename); - public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); + public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float density, float rate); public static native void onNativeResize(); public static native void onNativeKeyDown(int keycode); public static native void onNativeKeyUp(int keycode); @@ -920,18 +1075,30 @@ boolean sendCommand(int command, Object data) { public static native void onNativeTouch(int touchDevId, int pointerFingerId, int action, float x, float y, float p); + public static native void onNativePen(int penId, int device_type, int button, int action, float x, float y, float p); public static native void onNativeAccel(float x, float y, float z); public static native void onNativeClipboardChanged(); public static native void onNativeSurfaceCreated(); public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); + public static native void onNativeScreenKeyboardShown(); + public static native void onNativeScreenKeyboardHidden(); public static native String nativeGetHint(String name); public static native boolean nativeGetHintBoolean(String name, boolean default_value); public static native void nativeSetenv(String name, String value); - public static native void onNativeOrientationChanged(int orientation); + public static native void nativeSetNaturalOrientation(int orientation); + public static native void onNativeRotationChanged(int rotation); + public static native void onNativeInsetsChanged(int left, int right, int top, int bottom); public static native void nativeAddTouch(int touchId, String name); public static native void nativePermissionResult(int requestCode, boolean result); public static native void onNativeLocaleChanged(); + public static native void onNativeDarkModeChanged(boolean enabled); + public static native boolean nativeAllowRecreateActivity(); + public static native int nativeCheckSDLThreadCounter(); + public static native void onNativeFileDialog(int requestCode, String[] filelist, int filter); + public static native void onNativePinchStart(); + public static native void onNativePinchUpdate(float scale); + public static native void onNativePinchEnd(); /** * This method is called by SDL using JNI. @@ -969,9 +1136,14 @@ public void setOrientationBis(int w, int h, boolean resizable, String hint) int orientation_landscape = -1; int orientation_portrait = -1; + if (w <= 1 || h <= 1) { + // Invalid width/height, ignore this request + return; + } + /* If set, hint "explicitly controls which UI orientations are allowed". */ if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; } else if (hint.contains("LandscapeLeft")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } else if (hint.contains("LandscapeRight")) { @@ -982,7 +1154,7 @@ public void setOrientationBis(int w, int h, boolean resizable, String hint) boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); if (contains_Portrait && hint.contains("PortraitUpsideDown")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; } else if (contains_Portrait) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else if (hint.contains("PortraitUpsideDown")) { @@ -1046,44 +1218,9 @@ public static void minimizeWindow() { * This method is called by SDL using JNI. */ public static boolean shouldMinimizeOnFocusLoss() { -/* - if (Build.VERSION.SDK_INT >= 24) { - if (mSingleton == null) { - return true; - } - - if (mSingleton.isInMultiWindowMode()) { - return false; - } - - if (mSingleton.isInPictureInPictureMode()) { - return false; - } - } - - return true; -*/ return false; } - /** - * This method is called by SDL using JNI. - */ - public static boolean isScreenKeyboardShown() - { - if (mTextEdit == null) { - return false; - } - - if (!mScreenKeyboardShown) { - return false; - } - - InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - return imm.isAcceptingText(); - - } - /** * This method is called by SDL using JNI. */ @@ -1128,7 +1265,7 @@ public static boolean sendMessage(int command, int param) { /** * This method is called by SDL using JNI. */ - public static Context getContext() { + public static Activity getContext() { return SDL.getContext(); } @@ -1143,16 +1280,29 @@ public static boolean isAndroidTV() { if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { return true; } - if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { + if (Build.MANUFACTURER.equals("Amlogic") && + (Build.MODEL.startsWith("TV") || + Build.MODEL.equals("X96-W") || + Build.MODEL.equals("A95X-R1"))) { return true; } - return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); + return false; + } + + public static boolean isVRHeadset() { + if (Build.MANUFACTURER.equals("Oculus") && Build.MODEL.startsWith("Quest")) { + return true; + } + if (Build.MANUFACTURER.equals("Pico")) { + return true; + } + return false; } public static double getDiagonal() { DisplayMetrics metrics = new DisplayMetrics(); - Activity activity = (Activity)getContext(); + Activity activity = getContext(); if (activity == null) { return 0.0; } @@ -1176,10 +1326,17 @@ public static boolean isTablet() { * This method is called by SDL using JNI. */ public static boolean isChromebook() { - if (getContext() == null) { - return false; + // https://stackoverflow.com/questions/39784415/how-to-detect-programmatically-if-android-app-is-running-in-chrome-book-or-in + if (getContext() != null) { + if (getContext().getPackageManager().hasSystemFeature("org.chromium.arc") + || getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management")) { + return true; + } } - return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); + + // Running on AVD emulator + boolean isChromebookEmulator = (Build.MODEL != null && Build.MODEL.startsWith("sdk_gpc_")); + return isChromebookEmulator; } /** @@ -1199,13 +1356,6 @@ public static boolean isDeXMode() { } } - /** - * This method is called by SDL using JNI. - */ - public static DisplayMetrics getDisplayDPI() { - return getContext().getResources().getDisplayMetrics(); - } - /** * This method is called by SDL using JNI. */ @@ -1250,9 +1400,11 @@ static class ShowTextInputTask implements Runnable { */ static final int HEIGHT_PADDING = 15; + public int input_type; public int x, y, w, h; - public ShowTextInputTask(int x, int y, int w, int h) { + public ShowTextInputTask(int input_type, int x, int y, int w, int h) { + this.input_type = input_type; this.x = x; this.y = y; this.w = w; @@ -1274,29 +1426,32 @@ public void run() { params.topMargin = y; if (mTextEdit == null) { - mTextEdit = new DummyEdit(SDL.getContext()); + mTextEdit = new SDLDummyEdit(getContext()); mLayout.addView(mTextEdit, params); } else { mTextEdit.setLayoutParams(params); } + mTextEdit.setInputType(input_type); mTextEdit.setVisibility(View.VISIBLE); mTextEdit.requestFocus(); - InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mTextEdit, 0); - mScreenKeyboardShown = true; + if (imm.isAcceptingText()) { + onNativeScreenKeyboardShown(); + } } } /** * This method is called by SDL using JNI. */ - public static boolean showTextInput(int x, int y, int w, int h) { + public static boolean showTextInput(int input_type, int x, int y, int w, int h) { // Transfer the task to the main thread as a Runnable - return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); + return mSingleton.commandHandler.post(new ShowTextInputTask(input_type, x, y, w, h)); } public static boolean isTextInputEvent(KeyEvent event) { @@ -1336,31 +1491,37 @@ public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputC if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode, event.getScanCode())) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode, event.getScanCode())) { return true; } } } if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { - // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses - // they are ignored here because sending them as mouse input to SDL is messy - if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - case KeyEvent.ACTION_UP: - // mark the event as handled or it will be handled by system - // handling KEYCODE_BACK by system will call onBackPressed() - return true; + if (SDLActivity.isVRHeadset()) { + // The Oculus Quest controller back button comes in as source mouse, so accept that + } else { + // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses + // they are ignored here because sending them as mouse input to SDL is messy + if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + // mark the event as handled or it will be handled by system + // handling KEYCODE_BACK by system will call onBackPressed() + return true; + } } } } if (event.getAction() == KeyEvent.ACTION_DOWN) { + onNativeKeyDown(keyCode); + if (isTextInputEvent(event)) { if (ic != null) { ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); @@ -1368,7 +1529,6 @@ public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputC SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); } } - onNativeKeyDown(keyCode); return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { onNativeKeyUp(keyCode); @@ -1402,17 +1562,7 @@ public static void initTouch() { if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN || device.isVirtual())) { - int touchDevId = device.getId(); - /* - * 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; - } - nativeAddTouch(touchDevId, device.getName()); + nativeAddTouch(device.getId(), device.getName()); } } } @@ -1426,6 +1576,10 @@ public static void initTouch() { * This method is called by SDL using JNI. * Shows the messagebox from UI thread and block calling thread. * buttonFlags, buttonIds and buttonTexts must have same length. + * @param buttonFlags array containing flags for every button. + * @param buttonIds array containing id for every button. + * @param buttonTexts array containing text for every button. + * @param colors null for default or array of length 5 containing colors. * @return button id or -1. */ public int messageboxShowMessageBox( @@ -1617,16 +1771,14 @@ public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) { private final Runnable rehideSystemUi = new Runnable() { @Override public void run() { - if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { - int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; - - SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); - } + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; + + SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); } }; @@ -1754,10 +1906,34 @@ public static boolean setSystemCursor(int cursorID) { case SDL_SYSTEM_CURSOR_HAND: cursor_type = 1002; //PointerIcon.TYPE_HAND; break; + case SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT: + cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_TOP: + cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT: + cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_RIGHT: + cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT: + cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_BOTTOM: + cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT: + cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_LEFT: + cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; + break; } if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { try { - mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type)); + mSurface.setPointerIcon(PointerIcon.getSystemIcon(getContext(), cursor_type)); } catch (Exception e) { return false; } @@ -1774,7 +1950,7 @@ public static void requestPermission(String permission, int requestCode) { return; } - Activity activity = (Activity)getContext(); + Activity activity = getContext(); if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { activity.requestPermissions(new String[]{permission}, requestCode); } else { @@ -1791,44 +1967,41 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in /** * This method is called by SDL using JNI. */ - public static int openURL(String url) + public static boolean openURL(String url) { try { Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); - int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; - if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { - flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT; - } else { - flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET; - } + int flags = Intent.FLAG_ACTIVITY_NO_HISTORY + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK + | Intent.FLAG_ACTIVITY_NEW_DOCUMENT; i.addFlags(flags); mSingleton.startActivity(i); } catch (Exception ex) { - return -1; + return false; } - return 0; + return true; } /** * This method is called by SDL using JNI. */ - public static int showToast(String message, int duration, int gravity, int xOffset, int yOffset) + public static boolean showToast(String message, int duration, int gravity, int xOffset, int yOffset) { if(null == mSingleton) { - return - 1; + return false; } try { class OneShotTask implements Runnable { - String mMessage; - int mDuration; - int mGravity; - int mXOffset; - int mYOffset; + private final String mMessage; + private final int mDuration; + private final int mGravity; + private final int mXOffset; + private final int mYOffset; OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) { mMessage = message; @@ -1853,167 +2026,160 @@ public void run() { } mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset)); } catch(Exception ex) { - return -1; + return false; } - return 0; + return true; } -} -/** - Simple runnable to start the SDL application -*/ -class SDLMain implements Runnable { - @Override - public void run() { - // Runs SDL_main() - String library = SDLActivity.mSingleton.getMainSharedObject(); - String function = SDLActivity.mSingleton.getMainFunction(); - String[] arguments = SDLActivity.mSingleton.getArguments(); + /** + * This method is called by SDL using JNI. + */ + public static int openFileDescriptor(String uri, String mode) throws Exception { + if (mSingleton == null) { + return -1; + } try { - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); - } catch (Exception e) { - Log.v("SDL", "modify thread properties failed " + e.toString()); + ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); + return pfd != null ? pfd.detachFd() : -1; + } catch (FileNotFoundException e) { + e.printStackTrace(); + return -1; } + } - Log.v("SDL", "Running main function " + function + " from library " + library); - - SDLActivity.nativeRunMain(library, function, arguments); + /** + * This method is called by SDL using JNI. + */ + public static boolean showFileDialog(String[] filters, boolean allowMultiple, boolean forWrite, int requestCode) { + if (mSingleton == null) { + return false; + } - Log.v("SDL", "Finished main function"); + if (forWrite) { + allowMultiple = false; + } - if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { - // Let's finish the Activity - SDLActivity.mSDLThread = null; - SDLActivity.mSingleton.finish(); - } // else: Activity is already being destroyed + /* Convert string list of extensions to their respective MIME types */ + ArrayList mimes = new ArrayList<>(); + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + if (filters != null) { + for (String pattern : filters) { + String[] extensions = pattern.split(";"); - } -} + if (extensions.length == 1 && extensions[0].equals("*")) { + /* Handle "*" special case */ + mimes.add("*/*"); + } else { + for (String ext : extensions) { + String mime = mimeTypeMap.getMimeTypeFromExtension(ext); + if (mime != null) { + mimes.add(mime); + } + } + } + } + } -class SDLInputConnection extends BaseInputConnection { + /* Display the file dialog */ + Intent intent = new Intent(forWrite ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + switch (mimes.size()) { + case 0: + intent.setType("*/*"); + break; + case 1: + intent.setType(mimes.get(0)); + break; + default: + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{})); + } - protected EditText mEditText; - protected String mCommittedText = ""; + try { + mSingleton.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Unable to open file dialog.", e); + return false; + } - public SDLInputConnection(View targetView, boolean fullEditor) { - super(targetView, fullEditor); - mEditText = new EditText(SDL.getContext()); + /* Save current dialog state */ + mFileDialogState = new SDLFileDialogState(); + mFileDialogState.requestCode = requestCode; + mFileDialogState.multipleChoice = allowMultiple; + return true; } - @Override - public Editable getEditable() { - return mEditText.getEditableText(); + /* Internal class used to track active open file dialog */ + static class SDLFileDialogState { + int requestCode; + boolean multipleChoice; } - @Override - public boolean sendKeyEvent(KeyEvent event) { - /* - * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) - * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses - * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys - * that still do, we empty this out. - */ - - /* - * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key - * as we do with physical keyboards, let's just use it to hide the keyboard. - */ - - if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { - if (SDLActivity.onNativeSoftReturnKey()) { - return true; + /** + * This method is called by SDL using JNI. + */ + public static String getPreferredLocales() { + String result = ""; + if (Build.VERSION.SDK_INT >= 24 /* Android 7 (N) */) { + LocaleList locales = LocaleList.getAdjustedDefault(); + for (int i = 0; i < locales.size(); i++) { + if (i != 0) result += ","; + result += formatLocale(locales.get(i)); } + } else if (mCurrentLocale != null) { + result = formatLocale(mCurrentLocale); } - - return super.sendKeyEvent(event); + return result; } - @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - if (!super.commitText(text, newCursorPosition)) { - return false; + public static String formatLocale(Locale locale) { + String result = ""; + String lang = ""; + if (locale.getLanguage() == "in") { + // Indonesian is "id" according to ISO 639.2, but on Android is "in" because of Java backwards compatibility + lang = "id"; + } else if (locale.getLanguage() == "") { + // Make sure language is never empty + lang = "und"; + } else { + lang = locale.getLanguage(); } - updateText(); - return true; - } - @Override - public boolean setComposingText(CharSequence text, int newCursorPosition) { - if (!super.setComposingText(text, newCursorPosition)) { - return false; + if (locale.getCountry() == "") { + result = lang; + } else { + result = lang + "_" + locale.getCountry(); } - updateText(); - return true; + return result; } +} +/** + Simple runnable to start the SDL application +*/ +class SDLMain implements Runnable { @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; - } - } + public void run() { + // Runs SDLActivity.main() - if (!super.deleteSurroundingText(beforeLength, afterLength)) { - return false; + try { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); + } catch (Exception e) { + Log.v("SDL", "modify thread properties failed " + e.toString()); } - updateText(); - return true; - } - protected void updateText() { - final Editable content = getEditable(); - if (content == null) { - return; - } + SDLActivity.nativeInitMainThread(); + SDLActivity.mSingleton.main(); + SDLActivity.nativeCleanupMainThread(); - String text = content.toString(); - int compareLength = Math.min(text.length(), mCommittedText.length()); - int matchLength, offset; + if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { + // Let's finish the Activity + SDLActivity.mSDLThread = null; + SDLActivity.mSDLMainFinished = true; + SDLActivity.mSingleton.finish(); + } // else: Activity is already being destroyed - /* Backspace over characters that are no longer in the string */ - for (matchLength = 0; matchLength < compareLength; ) { - int codePoint = mCommittedText.codePointAt(matchLength); - if (codePoint != text.codePointAt(matchLength)) { - break; - } - matchLength += Character.charCount(codePoint); - } - /* FIXME: This doesn't handle graphemes, like '🌬️' */ - for (offset = matchLength; offset < mCommittedText.length(); ) { - int codePoint = mCommittedText.codePointAt(offset); - nativeGenerateScancodeForUnichar('\b'); - offset += Character.charCount(codePoint); - } - - if (matchLength < text.length()) { - String pendingText = text.subSequence(matchLength, text.length()).toString(); - for (offset = 0; offset < pendingText.length(); ) { - int codePoint = pendingText.codePointAt(offset); - if (codePoint == '\n') { - if (SDLActivity.onNativeSoftReturnKey()) { - return; - } - } - /* Higher code points don't generate simulated scancodes */ - if (codePoint < 128) { - nativeGenerateScancodeForUnichar((char)codePoint); - } - offset += Character.charCount(codePoint); - } - SDLInputConnection.nativeCommitText(pendingText, 0); - } - mCommittedText = text; } - - public static native void nativeCommitText(String text, int newCursorPosition); - - public static native void nativeGenerateScancodeForUnichar(char c); -} +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLAudioManager.java b/Knossos.NET.Android/java/org/libsdl/app/SDLAudioManager.java index 03cd4aa7..43706fa4 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLAudioManager.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLAudioManager.java @@ -3,31 +3,21 @@ import android.content.Context; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; -import android.media.AudioFormat; import android.media.AudioManager; -import android.media.AudioRecord; -import android.media.AudioTrack; -import android.media.MediaRecorder; import android.os.Build; import android.util.Log; import java.util.Arrays; import java.util.ArrayList; -public class SDLAudioManager { +class SDLAudioManager { protected static final String TAG = "SDLAudio"; - protected static AudioTrack mAudioTrack; - protected static AudioRecord mAudioRecord; protected static Context mContext; - private static final int[] NO_DEVICES = {}; - private static AudioDeviceCallback mAudioDeviceCallback; - public static void initialize() { - mAudioTrack = null; - mAudioRecord = null; + static void initialize() { mAudioDeviceCallback = null; if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) @@ -35,478 +25,85 @@ public static void initialize() { mAudioDeviceCallback = new AudioDeviceCallback() { @Override public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { - if (addedDevices != null) { - for (AudioDeviceInfo deviceInfo : addedDevices) { - if (deviceInfo != null) { - addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()); - } - } - } + for (AudioDeviceInfo deviceInfo : addedDevices) { + nativeAddAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId()); + } } @Override public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { - if (removedDevices != null) { - for (AudioDeviceInfo deviceInfo : removedDevices) { - if (deviceInfo != null) { - removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()); - } - } - } + for (AudioDeviceInfo deviceInfo : removedDevices) { + nativeRemoveAudioDevice(deviceInfo.isSink(), deviceInfo.getId()); + } } }; } } - public static void setContext(Context context) { + static void setContext(Context context) { mContext = context; - if (context != null) { - registerAudioDeviceCallback(); - } } - public static void release(Context context) { - unregisterAudioDeviceCallback(context); + static void release(Context context) { + // no-op atm } // Audio - protected static String getAudioFormatString(int audioFormat) { - switch (audioFormat) { - case AudioFormat.ENCODING_PCM_8BIT: - return "8-bit"; - case AudioFormat.ENCODING_PCM_16BIT: - return "16-bit"; - case AudioFormat.ENCODING_PCM_FLOAT: - return "float"; - default: - return Integer.toString(audioFormat); - } - } - - protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { - int channelConfig; - int sampleSize; - int frameSize; - - Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz"); - - /* On older devices let's use known good settings */ - if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) { - if (desiredChannels > 2) { - desiredChannels = 2; - } - } - - /* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */ - if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) { - if (sampleRate < 8000) { - sampleRate = 8000; - } else if (sampleRate > 48000) { - sampleRate = 48000; - } - } - - if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { - int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */); - if (Build.VERSION.SDK_INT < minSDKVersion) { - audioFormat = AudioFormat.ENCODING_PCM_16BIT; - } - } - switch (audioFormat) - { - case AudioFormat.ENCODING_PCM_8BIT: - sampleSize = 1; - break; - case AudioFormat.ENCODING_PCM_16BIT: - sampleSize = 2; - break; - case AudioFormat.ENCODING_PCM_FLOAT: - sampleSize = 4; - break; - default: - Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT"); - audioFormat = AudioFormat.ENCODING_PCM_16BIT; - sampleSize = 2; - break; - } - - if (isCapture) { - switch (desiredChannels) { - case 1: - channelConfig = AudioFormat.CHANNEL_IN_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_IN_STEREO; - break; - default: - Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); - desiredChannels = 2; - channelConfig = AudioFormat.CHANNEL_IN_STEREO; - break; - } - } else { - switch (desiredChannels) { - case 1: - channelConfig = AudioFormat.CHANNEL_OUT_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - case 3: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 4: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD; - break; - case 5: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 6: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - case 7: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; - break; - case 8: - if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { - channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; - } else { - Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); - desiredChannels = 6; - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - } - break; - default: - Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); - desiredChannels = 2; - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - } - -/* - Log.v(TAG, "Speaker configuration (and order of channels):"); - - if ((channelConfig & 0x00000004) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT"); - } - if ((channelConfig & 0x00000008) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT"); - } - if ((channelConfig & 0x00000010) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER"); - } - if ((channelConfig & 0x00000020) != 0) { - Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY"); - } - if ((channelConfig & 0x00000040) != 0) { - Log.v(TAG, " CHANNEL_OUT_BACK_LEFT"); - } - if ((channelConfig & 0x00000080) != 0) { - Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT"); - } - if ((channelConfig & 0x00000100) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER"); - } - if ((channelConfig & 0x00000200) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER"); - } - if ((channelConfig & 0x00000400) != 0) { - Log.v(TAG, " CHANNEL_OUT_BACK_CENTER"); - } - if ((channelConfig & 0x00000800) != 0) { - Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT"); - } - if ((channelConfig & 0x00001000) != 0) { - Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT"); - } -*/ - } - frameSize = (sampleSize * desiredChannels); - - // Let the user pick a larger buffer if they really want -- but ye - // gods they probably shouldn't, the minimums are horrifyingly high - // latency already - int minBufferSize; - if (isCapture) { - minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); - } else { - minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); - } - desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize); - - int[] results = new int[4]; - - if (isCapture) { - if (mAudioRecord == null) { - mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, - channelConfig, audioFormat, desiredFrames * frameSize); - - // see notes about AudioTrack state in audioOpen(), above. Probably also applies here. - if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { - Log.e(TAG, "Failed during initialization of AudioRecord"); - mAudioRecord.release(); - mAudioRecord = null; - return null; - } - - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { - mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId)); - } - - mAudioRecord.startRecording(); - } - - results[0] = mAudioRecord.getSampleRate(); - results[1] = mAudioRecord.getAudioFormat(); - results[2] = mAudioRecord.getChannelCount(); - - } else { - if (mAudioTrack == null) { - mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); - - // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid - // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java - // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() - if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - /* Try again, with safer values */ - - Log.e(TAG, "Failed during initialization of Audio Track"); - mAudioTrack.release(); - mAudioTrack = null; - return null; - } - - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { - mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId)); - } - - mAudioTrack.play(); - } - - results[0] = mAudioTrack.getSampleRate(); - results[1] = mAudioTrack.getAudioFormat(); - results[2] = mAudioTrack.getChannelCount(); - } - results[3] = desiredFrames; - - Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); - - return results; - } - private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) { if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) - //.filter(deviceInfo -> deviceInfo.getId() == deviceId) - .findFirst() - .orElse(null); - } else { - return null; - } - } - - private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) - //.filter(deviceInfo -> deviceInfo.getId() == deviceId) - .findFirst() - .orElse(null); - } else { - return null; - } - } - - private static void registerAudioDeviceCallback() { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null); - } - } - - private static void unregisterAudioDeviceCallback(Context context) { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback); - } - } - - /** - * This method is called by SDL using JNI. - */ - public static int[] getAudioOutputDevices() { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - //return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray(); - return NO_DEVICES; - } else { - return NO_DEVICES; + for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) { + if (deviceInfo.getId() == deviceId) { + return deviceInfo; + } + } } + return null; } - /** - * This method is called by SDL using JNI. - */ - public static int[] getAudioInputDevices() { + private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) { if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - //return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray(); - return NO_DEVICES; - } else { - return NO_DEVICES; - } - } - - /** - * This method is called by SDL using JNI. - */ - public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { - return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId); - } - - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteFloatBuffer(float[] buffer) { - if (mAudioTrack == null) { - Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); - return; - } - - if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) { - Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)"); - return; - } - - for (int i = 0; i < buffer.length;) { - int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom + for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (deviceInfo.getId() == deviceId) { + return deviceInfo; } - } else { - Log.w(TAG, "SDL audio: error return from write(float)"); - return; } } + return null; } - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteShortBuffer(short[] buffer) { - if (mAudioTrack == null) { - Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); - return; - } - - for (int i = 0; i < buffer.length;) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom + static void registerAudioDeviceCallback() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + // get an initial list now, before hotplug callbacks fire. + for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) { + continue; // Device cannot be opened } - } else { - Log.w(TAG, "SDL audio: error return from write(short)"); - return; + nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId()); } - } - } - - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteByteBuffer(byte[] buffer) { - if (mAudioTrack == null) { - Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); - return; - } - - for (int i = 0; i < buffer.length; ) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom - } - } else { - Log.w(TAG, "SDL audio: error return from write(byte)"); - return; + for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) { + nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId()); } + audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null); } } - /** - * This method is called by SDL using JNI. - */ - public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { - return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId); - } - - /** This method is called by SDL using JNI. */ - public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { - return 0; - } else { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); - } - } - - /** This method is called by SDL using JNI. */ - public static int captureReadShortBuffer(short[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { - return mAudioRecord.read(buffer, 0, buffer.length); - } else { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); - } - } - - /** This method is called by SDL using JNI. */ - public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { - return mAudioRecord.read(buffer, 0, buffer.length); - } else { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); - } - } - - /** This method is called by SDL using JNI. */ - public static void audioClose() { - if (mAudioTrack != null) { - mAudioTrack.stop(); - mAudioTrack.release(); - mAudioTrack = null; - } - } - - /** This method is called by SDL using JNI. */ - public static void captureClose() { - if (mAudioRecord != null) { - mAudioRecord.stop(); - mAudioRecord.release(); - mAudioRecord = null; + static void unregisterAudioDeviceCallback() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback); } } /** This method is called by SDL using JNI. */ - public static void audioSetThreadPriority(boolean iscapture, int device_id) { + static void audioSetThreadPriority(boolean recording, int device_id) { try { /* Set thread name */ - if (iscapture) { + if (recording) { Thread.currentThread().setName("SDLAudioC" + device_id); } else { Thread.currentThread().setName("SDLAudioP" + device_id); @@ -520,10 +117,10 @@ public static void audioSetThreadPriority(boolean iscapture, int device_id) { } } - public static native int nativeSetupJNI(); + static native void nativeSetupJNI(); - public static native void removeAudioDevice(boolean isCapture, int deviceId); + static native void nativeRemoveAudioDevice(boolean recording, int deviceId); - public static native void addAudioDevice(boolean isCapture, int deviceId); + static native void nativeAddAudioDevice(boolean recording, String name, int deviceId); } diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLClipboardHandler.java b/Knossos.NET.Android/java/org/libsdl/app/SDLClipboardHandler.java index aa20686d..a32be9af 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLClipboardHandler.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLClipboardHandler.java @@ -4,6 +4,7 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.UiModeManager; +import android.content.ActivityNotFoundException; import android.content.ClipboardManager; import android.content.ClipData; import android.content.Context; @@ -22,10 +23,9 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.LocaleList; import android.os.Message; -import android.text.Editable; -import android.text.InputType; -import android.text.Selection; +import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; @@ -39,22 +39,21 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; import android.widget.Button; -import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.Hashtable; import java.util.Locale; - public class SDLClipboardHandler implements ClipboardManager.OnPrimaryClipChangedListener { @@ -66,7 +65,11 @@ public class SDLClipboardHandler implements } public boolean clipboardHasText() { - return mClipMgr.hasPrimaryClip(); + if (Build.VERSION.SDK_INT >= 28 /* Android 9 (P) */) { + return mClipMgr.hasPrimaryClip(); + } else { + return mClipMgr.hasText(); + } } public String clipboardGetText() { @@ -84,14 +87,23 @@ public String clipboardGetText() { } public void clipboardSetText(String string) { - mClipMgr.removePrimaryClipChangedListener(this); - ClipData clip = ClipData.newPlainText(null, string); - mClipMgr.setPrimaryClip(clip); - mClipMgr.addPrimaryClipChangedListener(this); + mClipMgr.removePrimaryClipChangedListener(this); + if (string.isEmpty()) { + if (Build.VERSION.SDK_INT >= 28 /* Android 9 (P) */) { + mClipMgr.clearPrimaryClip(); + } else { + ClipData clip = ClipData.newPlainText(null, ""); + mClipMgr.setPrimaryClip(clip); + } + } else { + ClipData clip = ClipData.newPlainText(null, string); + mClipMgr.setPrimaryClip(clip); + } + mClipMgr.addPrimaryClipChangedListener(this); } @Override public void onPrimaryClipChanged() { SDLActivity.onNativeClipboardChanged(); } -} +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLControllerManager.java b/Knossos.NET.Android/java/org/libsdl/app/SDLControllerManager.java index 81511c56..a9b4f418 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLControllerManager.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLControllerManager.java @@ -1,12 +1,24 @@ package org.libsdl.app; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; + import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorManager; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; @@ -17,38 +29,38 @@ public class SDLControllerManager { - public static native int nativeSetupJNI(); - - public static native int nativeAddJoystick(int device_id, String name, String desc, - int vendor_id, int product_id, - boolean is_accelerometer, int button_mask, - int naxes, int axis_mask, int nhats, int nballs); - public static native int nativeRemoveJoystick(int device_id); - public static native int nativeAddHaptic(int device_id, String name); - public static native int nativeRemoveHaptic(int device_id); - public static native int onNativePadDown(int device_id, int keycode); - public static native int onNativePadUp(int device_id, int keycode); - public static native void onNativeJoy(int device_id, int axis, + static native void nativeSetupJNI(); + + static native void nativeAddJoystick(int device_id, String name, String desc, + int vendor_id, int product_id, + int button_mask, + int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led, + boolean has_accelerometer, boolean has_gyroscope); + static native void nativeRemoveJoystick(int device_id); + static native void nativeAddHaptic(int device_id, String name); + static native void nativeRemoveHaptic(int device_id); + static public native boolean onNativePadDown(int device_id, int keycode, int scancode); + static public native boolean onNativePadUp(int device_id, int keycode, int scancode); + static native void onNativeJoy(int device_id, int axis, float value); - public static native void onNativeHat(int device_id, int hat_id, + static native void onNativeHat(int device_id, int hat_id, int x, int y); + static native void onNativeJoySensor(int device_id, int sensor_type, long sensor_timestamp, float x, float y, float z); protected static SDLJoystickHandler mJoystickHandler; protected static SDLHapticHandler mHapticHandler; private static final String TAG = "SDLControllerManager"; - public static void initialize() { + static void initialize() { if (mJoystickHandler == null) { - if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { - mJoystickHandler = new SDLJoystickHandler_API19(); - } else { - mJoystickHandler = new SDLJoystickHandler_API16(); - } + mJoystickHandler = new SDLJoystickHandler(); } if (mHapticHandler == null) { - if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + mHapticHandler = new SDLHapticHandler_API31(); + } else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { mHapticHandler = new SDLHapticHandler_API26(); } else { mHapticHandler = new SDLHapticHandler(); @@ -57,41 +69,62 @@ public static void initialize() { } // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance - public static boolean handleJoystickMotionEvent(MotionEvent event) { + static public boolean handleJoystickMotionEvent(MotionEvent event) { return mJoystickHandler.handleMotionEvent(event); } /** * This method is called by SDL using JNI. */ - public static void pollInputDevices() { + static void pollInputDevices() { mJoystickHandler.pollInputDevices(); } /** * This method is called by SDL using JNI. */ - public static void pollHapticDevices() { + static void joystickSetLED(int device_id, int red, int green, int blue) { + mJoystickHandler.setLED(device_id, red, green, blue); + } + + /** + * This method is called by SDL using JNI. + */ + static void joystickSetSensorsEnabled(int device_id, boolean enabled) { + mJoystickHandler.setSensorsEnabled(device_id, enabled); + } + + /** + * This method is called by SDL using JNI. + */ + static void pollHapticDevices() { mHapticHandler.pollHapticDevices(); } /** * This method is called by SDL using JNI. */ - public static void hapticRun(int device_id, float intensity, int length) { + static void hapticRun(int device_id, float intensity, int length) { mHapticHandler.run(device_id, intensity, length); } /** * This method is called by SDL using JNI. */ - public static void hapticStop(int device_id) + static void hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { + mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_intensity, length); + } + + /** + * This method is called by SDL using JNI. + */ + static void hapticStop(int device_id) { mHapticHandler.stop(device_id); } // Check if a given device is considered a possible SDL joystick - public static boolean isDeviceSDLJoystick(int deviceId) { + static public boolean isDeviceSDLJoystick(int deviceId) { InputDevice device = InputDevice.getDevice(deviceId); // We cannot use InputDevice.isVirtual before API 16, so let's accept // only nonnegative device ids (VIRTUAL_KEYBOARD equals -1) @@ -119,175 +152,4 @@ public static boolean isDeviceSDLJoystick(int deviceId) { ); } -} - - - -class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 { - // Generic Motion (mouse hover, joystick...) events go here - - private boolean mRelativeModeEnabled; - - @Override - public boolean onGenericMotion(View v, MotionEvent event) { - - // Handle relative mouse mode - if (mRelativeModeEnabled) { - if (event.getSource() == InputDevice.SOURCE_MOUSE) { - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_HOVER_MOVE) { - float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); - float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); - SDLActivity.onNativeMouse(0, action, x, y, true); - return true; - } - } - } - - // Event was not managed, call SDLGenericMotionListener_API12 method - return super.onGenericMotion(v, event); - } - - @Override - public boolean supportsRelativeMouse() { - return true; - } - - @Override - public boolean inRelativeMode() { - return mRelativeModeEnabled; - } - - @Override - public boolean setRelativeMouseEnabled(boolean enabled) { - mRelativeModeEnabled = enabled; - return true; - } - - @Override - public float getEventX(MotionEvent event) { - if (mRelativeModeEnabled) { - return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); - } else { - return event.getX(0); - } - } - - @Override - public float getEventY(MotionEvent event) { - if (mRelativeModeEnabled) { - return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); - } else { - return event.getY(0); - } - } -} - -class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { - // Generic Motion (mouse hover, joystick...) events go here - private boolean mRelativeModeEnabled; - - @Override - public boolean onGenericMotion(View v, MotionEvent event) { - float x, y; - int action; - - switch ( event.getSource() ) { - case InputDevice.SOURCE_JOYSTICK: - return SDLControllerManager.handleJoystickMotionEvent(event); - - case InputDevice.SOURCE_MOUSE: - // DeX desktop mouse cursor is a separate non-standard input type. - case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN: - action = event.getActionMasked(); - 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: - x = event.getX(0); - y = event.getY(0); - SDLActivity.onNativeMouse(0, action, x, y, false); - return true; - - default: - break; - } - break; - - case InputDevice.SOURCE_MOUSE_RELATIVE: - action = event.getActionMasked(); - 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: - x = event.getX(0); - y = event.getY(0); - SDLActivity.onNativeMouse(0, action, x, y, true); - return true; - - default: - break; - } - break; - - default: - break; - } - - // Event was not managed - return false; - } - - @Override - public boolean supportsRelativeMouse() { - return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */); - } - - @Override - public boolean inRelativeMode() { - return mRelativeModeEnabled; - } - - @Override - public boolean setRelativeMouseEnabled(boolean enabled) { - if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) { - if (enabled) { - SDLActivity.getContentView().requestPointerCapture(); - } else { - SDLActivity.getContentView().releasePointerCapture(); - } - mRelativeModeEnabled = enabled; - return true; - } else { - return false; - } - } - - @Override - public void reclaimRelativeMouseModeIfNeeded() - { - if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) { - SDLActivity.getContentView().requestPointerCapture(); - } - } - - @Override - public float getEventX(MotionEvent event) { - // Relative mouse in capture mode will only have relative for X/Y - return event.getX(0); - } - - @Override - public float getEventY(MotionEvent event) { - // Relative mouse in capture mode will only have relative for X/Y - return event.getY(0); - } -} +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/DummyEdit.java b/Knossos.NET.Android/java/org/libsdl/app/SDLDummyEdit.java similarity index 53% rename from Knossos.NET.Android/java/org/libsdl/app/DummyEdit.java rename to Knossos.NET.Android/java/org/libsdl/app/SDLDummyEdit.java index 69b18c64..61d8fe85 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/DummyEdit.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLDummyEdit.java @@ -1,72 +1,30 @@ package org.libsdl.app; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.UiModeManager; -import android.content.ClipboardManager; -import android.content.ClipData; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.hardware.Sensor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.text.Editable; +import android.content.*; import android.text.InputType; -import android.text.Selection; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.SparseArray; -import android.view.Display; -import android.view.Gravity; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.PointerIcon; -import android.view.Surface; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; +import android.view.*; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.Hashtable; -import java.util.Locale; - /* This is a fake invisible editor view that receives the input and defines the * pan&scan region */ -public class DummyEdit extends View implements View.OnKeyListener { +public class SDLDummyEdit extends View implements View.OnKeyListener +{ InputConnection ic; + int input_type; - public DummyEdit(Context context) { + SDLDummyEdit(Context context) { super(context); setFocusableInTouchMode(true); setFocusable(true); setOnKeyListener(this); } + void setInputType(int input_type) { + this.input_type = input_type; + } + @Override public boolean onCheckIsTextEditor() { return true; @@ -98,11 +56,11 @@ public boolean onKeyPreIme (int keyCode, KeyEvent event) { public InputConnection onCreateInputConnection(EditorInfo outAttrs) { ic = new SDLInputConnection(this, true); - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_FLAG_MULTI_LINE; + outAttrs.inputType = input_type; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; return ic; } -} \ No newline at end of file +} + diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API12.java b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API12.java deleted file mode 100644 index 92792183..00000000 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API12.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.libsdl.app; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import android.content.Context; -import android.os.Build; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.util.Log; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; - -public class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener { - // Generic Motion (mouse hover, joystick...) events go here - @Override - public boolean onGenericMotion(View v, MotionEvent event) { - float x, y; - int action; - - switch ( event.getSource() ) { - case InputDevice.SOURCE_JOYSTICK: - return SDLControllerManager.handleJoystickMotionEvent(event); - - case InputDevice.SOURCE_MOUSE: - action = event.getActionMasked(); - 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: - x = event.getX(0); - y = event.getY(0); - - SDLActivity.onNativeMouse(0, action, x, y, false); - return true; - - default: - break; - } - break; - - default: - break; - } - - // Event was not managed - return false; - } - - public boolean supportsRelativeMouse() { - return false; - } - - public boolean inRelativeMode() { - return false; - } - - public boolean setRelativeMouseEnabled(boolean enabled) { - return false; - } - - public void reclaimRelativeMouseModeIfNeeded() - { - - } - - public float getEventX(MotionEvent event) { - return event.getX(0); - } - - public float getEventY(MotionEvent event) { - return event.getY(0); - } - -} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API14.java b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API14.java new file mode 100644 index 00000000..0adefea6 --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API14.java @@ -0,0 +1,128 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public class SDLGenericMotionListener_API14 implements View.OnGenericMotionListener { + protected static final int SDL_PEN_DEVICE_TYPE_UNKNOWN = 0; + protected static final int SDL_PEN_DEVICE_TYPE_DIRECT = 1; + protected static final int SDL_PEN_DEVICE_TYPE_INDIRECT = 2; + + // Generic Motion (mouse hover, joystick...) events go here + @Override + public boolean onGenericMotion(View v, MotionEvent event) { + if (event.getSource() == InputDevice.SOURCE_JOYSTICK) + return SDLControllerManager.handleJoystickMotionEvent(event); + + float x, y; + int action = event.getActionMasked(); + int pointerCount = event.getPointerCount(); + boolean consumed = false; + + for (int i = 0; i < pointerCount; i++) { + int toolType = event.getToolType(i); + + if (toolType == MotionEvent.TOOL_TYPE_MOUSE) { + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i); + SDLActivity.onNativeMouse(0, action, x, y, false); + consumed = true; + break; + + case MotionEvent.ACTION_HOVER_MOVE: + x = getEventX(event, i); + y = getEventY(event, i); + + SDLActivity.onNativeMouse(0, action, x, y, checkRelativeEvent(event)); + consumed = true; + break; + + default: + break; + } + } else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) { + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_HOVER_EXIT: + x = event.getX(i); + y = event.getY(i); + float 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; + } + + // BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP + int buttons = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30)); + if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) { + buttons |= 0x08; + } + + SDLActivity.onNativePen(event.getPointerId(i), getPenDeviceType(event.getDevice()), buttons, action, x, y, p); + consumed = true; + break; + } + } + } + + return consumed; + } + + boolean supportsRelativeMouse() { + return false; + } + + boolean inRelativeMode() { + return false; + } + + boolean setRelativeMouseEnabled(boolean enabled) { + return false; + } + + void reclaimRelativeMouseModeIfNeeded() { + + } + + boolean checkRelativeEvent(MotionEvent event) { + return inRelativeMode(); + } + + float getEventX(MotionEvent event, int pointerIndex) { + return event.getX(pointerIndex); + } + + float getEventY(MotionEvent event, int pointerIndex) { + return event.getY(pointerIndex); + } + + int getPenDeviceType(InputDevice penDevice) { + return SDL_PEN_DEVICE_TYPE_UNKNOWN; + } +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API24.java b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API24.java new file mode 100644 index 00000000..f0ba90f5 --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API24.java @@ -0,0 +1,76 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API14 { + // Generic Motion (mouse hover, joystick...) events go here + + private boolean mRelativeModeEnabled; + + @Override + boolean supportsRelativeMouse() { + return true; + } + + @Override + boolean inRelativeMode() { + return mRelativeModeEnabled; + } + + @Override + boolean setRelativeMouseEnabled(boolean enabled) { + mRelativeModeEnabled = enabled; + return true; + } + + @Override + float getEventX(MotionEvent event, int pointerIndex) { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + /* Silence 'lint' warning */ + return 0; + } + + if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) { + return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X, pointerIndex); + } else { + return event.getX(pointerIndex); + } + } + + @Override + float getEventY(MotionEvent event, int pointerIndex) { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + /* Silence 'lint' warning */ + return 0; + } + + if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) { + return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y, pointerIndex); + } else { + return event.getY(pointerIndex); + } + } +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API26.java b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API26.java new file mode 100644 index 00000000..8ea13716 --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API26.java @@ -0,0 +1,96 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { + // Generic Motion (mouse hover, joystick...) events go here + private boolean mRelativeModeEnabled; + + @Override + boolean supportsRelativeMouse() { + return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */); + } + + @Override + boolean inRelativeMode() { + return mRelativeModeEnabled; + } + + @Override + boolean setRelativeMouseEnabled(boolean enabled) { + + if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) { + /* Silence 'lint' warning */ + return false; + } + + if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) { + if (enabled) { + SDLActivity.getContentView().requestPointerCapture(); + } else { + SDLActivity.getContentView().releasePointerCapture(); + } + mRelativeModeEnabled = enabled; + return true; + } else { + return false; + } + } + + @Override + void reclaimRelativeMouseModeIfNeeded() { + + if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) { + /* Silence 'lint' warning */ + return; + } + + if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) { + SDLActivity.getContentView().requestPointerCapture(); + } + } + + @Override + boolean checkRelativeEvent(MotionEvent event) { + if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) { + /* Silence 'lint' warning */ + return false; + } + return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE; + } + + @Override + float getEventX(MotionEvent event, int pointerIndex) { + // Relative mouse in capture mode will only have relative for X/Y + return event.getX(pointerIndex); + } + + @Override + float getEventY(MotionEvent event, int pointerIndex) { + // Relative mouse in capture mode will only have relative for X/Y + return event.getY(pointerIndex); + } +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API29.java b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API29.java new file mode 100644 index 00000000..142540e7 --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLGenericMotionListener_API29.java @@ -0,0 +1,38 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public class SDLGenericMotionListener_API29 extends SDLGenericMotionListener_API26 { + @Override + int getPenDeviceType(InputDevice penDevice) + { + if (penDevice == null) { + return SDL_PEN_DEVICE_TYPE_UNKNOWN; + } + + return penDevice.isExternal() ? SDL_PEN_DEVICE_TYPE_INDIRECT : SDL_PEN_DEVICE_TYPE_DIRECT; + } +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLHaptic.java b/Knossos.NET.Android/java/org/libsdl/app/SDLHaptic.java new file mode 100644 index 00000000..459772bd --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLHaptic.java @@ -0,0 +1,33 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + + +public class SDLHaptic { + int device_id; + String name; + Vibrator vib; +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler.java b/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler.java index a3ce10f3..f18d4d79 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler.java @@ -6,9 +6,19 @@ import java.util.List; import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorManager; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; @@ -17,61 +27,35 @@ public class SDLHapticHandler { - public static class SDLHaptic { - public int device_id; - public String name; - public Vibrator vib; - } - private final ArrayList mHaptics; - public SDLHapticHandler() { + SDLHapticHandler() { mHaptics = new ArrayList(); } - public void run(int device_id, float intensity, int length) { + void run(int device_id, float intensity, int length) { SDLHaptic haptic = getHaptic(device_id); if (haptic != null) { haptic.vib.vibrate(length); } } - public void stop(int device_id) { + void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { + // Not supported in older APIs + } + + void stop(int device_id) { SDLHaptic haptic = getHaptic(device_id); if (haptic != null) { haptic.vib.cancel(); } } - public void pollHapticDevices() { + synchronized void pollHapticDevices() { final int deviceId_VIBRATOR_SERVICE = 999999; boolean hasVibratorService = false; - int[] deviceIds = InputDevice.getDeviceIds(); - // It helps processing the device ids in reverse order - // For example, in the case of the XBox 360 wireless dongle, - // so the first controller seen by SDL matches what the receiver - // considers to be the first controller - - for (int i = deviceIds.length - 1; i > -1; i--) { - SDLHaptic haptic = getHaptic(deviceIds[i]); - if (haptic == null) { - InputDevice device = InputDevice.getDevice(deviceIds[i]); - Vibrator vib = device.getVibrator(); - if (vib != null) { - if (vib.hasVibrator()) { - haptic = new SDLHaptic(); - haptic.device_id = deviceIds[i]; - haptic.name = device.getName(); - haptic.vib = vib; - mHaptics.add(haptic); - SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); - } - } - } - } - /* Check VIBRATOR_SERVICE */ Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE); if (vib != null) { @@ -94,18 +78,11 @@ public void pollHapticDevices() { ArrayList removedDevices = null; for (SDLHaptic haptic : mHaptics) { int device_id = haptic.device_id; - int i; - for (i = 0; i < deviceIds.length; i++) { - if (device_id == deviceIds[i]) break; - } - if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) { - if (i == deviceIds.length) { - if (removedDevices == null) { - removedDevices = new ArrayList(); - } - removedDevices.add(device_id); + if (removedDevices == null) { + removedDevices = new ArrayList(); } + removedDevices.add(device_id); } // else: don't remove the vibrator if it is still present } @@ -122,7 +99,7 @@ public void pollHapticDevices() { } } - protected SDLHaptic getHaptic(int device_id) { + synchronized protected SDLHaptic getHaptic(int device_id) { for (SDLHaptic haptic : mHaptics) { if (haptic.device_id == device_id) { return haptic; @@ -130,4 +107,4 @@ protected SDLHaptic getHaptic(int device_id) { } return null; } -} \ No newline at end of file +} diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API26.java b/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API26.java index f07e1ba8..f53e37b3 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API26.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API26.java @@ -6,21 +6,35 @@ import java.util.List; import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorManager; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; - public class SDLHapticHandler_API26 extends SDLHapticHandler { @Override - public void run(int device_id, float intensity, int length) { + void run(int device_id, float intensity, int length) { + + if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) { + /* Silence 'lint' warning */ + return; + } + SDLHaptic haptic = getHaptic(device_id); if (haptic != null) { - Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length); if (intensity == 0.0f) { stop(device_id); return; diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API31.java b/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API31.java new file mode 100644 index 00000000..268f72dd --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLHapticHandler_API31.java @@ -0,0 +1,88 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +public class SDLHapticHandler_API31 extends SDLHapticHandler { + @Override + void run(int device_id, float intensity, int length) { + SDLHaptic haptic = getHaptic(device_id); + if (haptic != null) { + vibrate(haptic.vib, intensity, length); + } + } + + @Override + void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { + InputDevice device = InputDevice.getDevice(device_id); + if (device == null) { + return; + } + + if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) { + /* Silence 'lint' warning */ + return; + } + + VibratorManager manager = device.getVibratorManager(); + int[] vibrators = manager.getVibratorIds(); + if (vibrators.length >= 2) { + vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length); + vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length); + } else if (vibrators.length == 1) { + float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f); + vibrate(manager.getVibrator(vibrators[0]), intensity, length); + } + } + + private void vibrate(Vibrator vibrator, float intensity, int length) { + + if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) { + /* Silence 'lint' warning */ + return; + } + + if (intensity == 0.0f) { + vibrator.cancel(); + return; + } + + int value = Math.round(intensity * 255); + if (value > 255) { + value = 255; + } + if (value < 1) { + vibrator.cancel(); + return; + } + try { + vibrator.vibrate(VibrationEffect.createOneShot(length, value)); + } + catch (Exception e) { + // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if + // something went horribly wrong with the Android 8.0 APIs. + vibrator.vibrate(length); + } + } +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLInputConnection.java b/Knossos.NET.Android/java/org/libsdl/app/SDLInputConnection.java new file mode 100644 index 00000000..fdc2994c --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLInputConnection.java @@ -0,0 +1,136 @@ +package org.libsdl.app; + +import android.content.*; +import android.os.Build; +import android.text.Editable; +import android.view.*; +import android.view.inputmethod.BaseInputConnection; +import android.widget.EditText; + +class SDLInputConnection extends BaseInputConnection +{ + protected EditText mEditText; + protected String mCommittedText = ""; + + SDLInputConnection(View targetView, boolean fullEditor) { + super(targetView, fullEditor); + mEditText = new EditText(SDL.getContext()); + } + + @Override + public Editable getEditable() { + return mEditText.getEditableText(); + } + + @Override + public boolean sendKeyEvent(KeyEvent event) { + /* + * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) + * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses + * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys + * that still do, we empty this out. + */ + + /* + * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key + * as we do with physical keyboards, let's just use it to hide the keyboard. + */ + + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + if (SDLActivity.onNativeSoftReturnKey()) { + return true; + } + } + + return super.sendKeyEvent(event); + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + if (!super.commitText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (!super.setComposingText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + // 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; + } + updateText(); + return true; + } + + protected void updateText() { + final Editable content = getEditable(); + if (content == null) { + return; + } + + String text = content.toString(); + int compareLength = Math.min(text.length(), mCommittedText.length()); + int matchLength, offset; + + /* Backspace over characters that are no longer in the string */ + for (matchLength = 0; matchLength < compareLength; ) { + int codePoint = mCommittedText.codePointAt(matchLength); + if (codePoint != text.codePointAt(matchLength)) { + break; + } + matchLength += Character.charCount(codePoint); + } + /* FIXME: This doesn't handle graphemes, like '🌬️' */ + for (offset = matchLength; offset < mCommittedText.length(); ) { + int codePoint = mCommittedText.codePointAt(offset); + nativeGenerateScancodeForUnichar('\b'); + offset += Character.charCount(codePoint); + } + + if (matchLength < text.length()) { + String pendingText = text.subSequence(matchLength, text.length()).toString(); + if (!SDLActivity.dispatchingKeyEvent()) { + for (offset = 0; offset < pendingText.length(); ) { + int codePoint = pendingText.codePointAt(offset); + if (codePoint == '\n') { + if (SDLActivity.onNativeSoftReturnKey()) { + return; + } + } + /* Higher code points don't generate simulated scancodes */ + if (codePoint > 0 && codePoint < 128) { + nativeGenerateScancodeForUnichar((char)codePoint); + } + offset += Character.charCount(codePoint); + } + } + SDLInputConnection.nativeCommitText(pendingText, 0); + } + mCommittedText = text; + } + + public static native void nativeCommitText(String text, int newCursorPosition); + + public static native void nativeGenerateScancodeForUnichar(char c); +} + diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLJoySensorListener.java b/Knossos.NET.Android/java/org/libsdl/app/SDLJoySensorListener.java new file mode 100644 index 00000000..0490f031 --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLJoySensorListener.java @@ -0,0 +1,42 @@ +package org.libsdl.app; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; + +public class SDLJoySensorListener implements SensorEventListener { + int device_id; + + public SDLJoySensorListener(int device_id) { + this.device_id = device_id; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + @Override + public void onSensorChanged(SensorEvent event) { + SDLControllerManager.onNativeJoySensor(device_id, event.sensor.getType(), event.timestamp, event.values[0], event.values[1], event.values[2]); + } +} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler.java b/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler.java index d6b5251d..46dc6e09 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler.java @@ -6,29 +6,435 @@ import java.util.List; import android.content.Context; +import android.hardware.lights.Light; +import android.hardware.lights.LightsRequest; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.graphics.Color; import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorManager; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +/* Actual joystick functionality available for API >= 19 devices */ public class SDLJoystickHandler { + public static class SDLJoystick { + int device_id; + String name; + String desc; + ArrayList axes; + ArrayList hats; + ArrayList lights; + LightsManager.LightsSession lightsSession; + SensorManager sensorManager; + SDLJoySensorListener sensorListener; + Sensor accelerometerSensor; + Sensor gyroscopeSensor; + } + static class RangeComparator implements Comparator { + @Override + public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { + // Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL + int arg0Axis = arg0.getAxis(); + int arg1Axis = arg1.getAxis(); + if (arg0Axis == MotionEvent.AXIS_GAS) { + arg0Axis = MotionEvent.AXIS_BRAKE; + } else if (arg0Axis == MotionEvent.AXIS_BRAKE) { + arg0Axis = MotionEvent.AXIS_GAS; + } + if (arg1Axis == MotionEvent.AXIS_GAS) { + arg1Axis = MotionEvent.AXIS_BRAKE; + } else if (arg1Axis == MotionEvent.AXIS_BRAKE) { + arg1Axis = MotionEvent.AXIS_GAS; + } + + // Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ. + // This is because the usual pairing are: + // - AXIS_X + AXIS_Y (left stick). + // - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers). + // - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers). + // This sorts the axes in the above order, which tends to be correct + // for Xbox-ish game pads that have the right stick on RX/RY and the + // triggers on Z/RZ. + // + // Gamepads that don't have AXIS_Z/AXIS_RZ but use + // AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this. + // + // References: + // - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input + // - https://www.kernel.org/doc/html/latest/input/gamepad.html + if (arg0Axis == MotionEvent.AXIS_Z) { + arg0Axis = MotionEvent.AXIS_RZ - 1; + } else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) { + --arg0Axis; + } + if (arg1Axis == MotionEvent.AXIS_Z) { + arg1Axis = MotionEvent.AXIS_RZ - 1; + } else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) { + --arg1Axis; + } + + return arg0Axis - arg1Axis; + } + } + + private final ArrayList mJoysticks; + + SDLJoystickHandler() { + + mJoysticks = new ArrayList(); + } + + /** + * Handles adding and removing of input devices. + */ + synchronized void pollInputDevices() { + int[] deviceIds = InputDevice.getDeviceIds(); + + for (int device_id : deviceIds) { + if (SDLControllerManager.isDeviceSDLJoystick(device_id)) { + SDLJoystick joystick = getJoystick(device_id); + if (joystick == null) { + InputDevice joystickDevice = InputDevice.getDevice(device_id); + joystick = new SDLJoystick(); + joystick.device_id = device_id; + joystick.name = joystickDevice.getName(); + joystick.desc = getJoystickDescriptor(joystickDevice); + joystick.axes = new ArrayList(); + joystick.hats = new ArrayList(); + java.util.Set axisStrsSet = new java.util.HashSet(); + joystick.lights = new ArrayList(); + + List ranges = joystickDevice.getMotionRanges(); + Collections.sort(ranges, new RangeComparator()); + for (InputDevice.MotionRange range : ranges) { + if (((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) && axisStrsSet.add(range.getAxis())) { + if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { + joystick.hats.add(range); + } else { + joystick.axes.add(range); + } + } + } + + boolean can_rumble = false; + boolean has_rgb_led = false; + boolean has_accelerometer = false; + boolean has_gyroscope = false; + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + VibratorManager vibratorManager = joystickDevice.getVibratorManager(); + int[] vibrators = vibratorManager.getVibratorIds(); + if (vibrators.length > 0) { + can_rumble = true; + } + LightsManager lightsManager = joystickDevice.getLightsManager(); + List lights = lightsManager.getLights(); + for (Light light : lights) { + if (light.hasRgbControl()) { + joystick.lights.add(light); + } + } + if (!joystick.lights.isEmpty()) { + joystick.lightsSession = lightsManager.openSession(); + has_rgb_led = true; + } + SensorManager sensorManager = joystickDevice.getSensorManager(); + if (sensorManager != null) { + joystick.sensorManager = sensorManager; + joystick.sensorListener = new SDLJoySensorListener(joystick.device_id); + joystick.accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (joystick.accelerometerSensor != null) { + has_accelerometer = true; + } + joystick.gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (joystick.gyroscopeSensor != null) { + has_gyroscope = true; + } + } + } + + mJoysticks.add(joystick); + SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, + getVendorId(joystickDevice), getProductId(joystickDevice), + getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led, + has_accelerometer, has_gyroscope); + } + } + } + + /* Check removed devices */ + ArrayList removedDevices = null; + for (SDLJoystick joystick : mJoysticks) { + int device_id = joystick.device_id; + int i; + for (i = 0; i < deviceIds.length; i++) { + if (device_id == deviceIds[i]) break; + } + if (i == deviceIds.length) { + if (removedDevices == null) { + removedDevices = new ArrayList(); + } + removedDevices.add(device_id); + } + } + + if (removedDevices != null) { + for (int device_id : removedDevices) { + SDLControllerManager.nativeRemoveJoystick(device_id); + for (int i = 0; i < mJoysticks.size(); i++) { + if (mJoysticks.get(i).device_id == device_id) { + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + if (mJoysticks.get(i).lightsSession != null) { + try { + mJoysticks.get(i).lightsSession.close(); + } catch (Exception e) { + // Session may already be unregistered when device disconnects + } + mJoysticks.get(i).lightsSession = null; + } + } + mJoysticks.remove(i); + break; + } + } + } + } + } + + synchronized protected SDLJoystick getJoystick(int device_id) { + for (SDLJoystick joystick : mJoysticks) { + if (joystick.device_id == device_id) { + return joystick; + } + } + return null; + } + /** * Handles given MotionEvent. * @param event the event to be handled. * @return if given event was processed. */ - public boolean handleMotionEvent(MotionEvent event) { - return false; + boolean handleMotionEvent(MotionEvent event) { + int actionPointerIndex = event.getActionIndex(); + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_MOVE) { + SDLJoystick joystick = getJoystick(event.getDeviceId()); + if (joystick != null) { + for (int i = 0; i < joystick.axes.size(); i++) { + InputDevice.MotionRange range = joystick.axes.get(i); + /* Normalize the value to -1...1 */ + float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f; + SDLControllerManager.onNativeJoy(joystick.device_id, i, value); + } + for (int i = 0; i < joystick.hats.size() / 2; i++) { + int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex)); + int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex)); + SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY); + } + } + } + return true; } - /** - * Handles adding and removing of input devices. - */ - public void pollInputDevices() { + String getJoystickDescriptor(InputDevice joystickDevice) { + String desc = joystickDevice.getDescriptor(); + + if (desc != null && !desc.isEmpty()) { + return desc; + } + + return joystickDevice.getName(); + } + + int getProductId(InputDevice joystickDevice) { + return joystickDevice.getProductId(); + } + + int getVendorId(InputDevice joystickDevice) { + return joystickDevice.getVendorId(); + } + + int getAxisMask(List ranges) { + // For compatibility, keep computing the axis mask like before, + // only really distinguishing 2, 4 and 6 axes. + int axis_mask = 0; + if (ranges.size() >= 2) { + // ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY)) + axis_mask |= 0x0003; + } + if (ranges.size() >= 4) { + // ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY)) + axis_mask |= 0x000c; + } + if (ranges.size() >= 6) { + // ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) + axis_mask |= 0x0030; + } + // Also add an indicator bit for whether the sorting order has changed. + // This serves to disable outdated gamecontrollerdb.txt mappings. + boolean have_z = false; + boolean have_past_z_before_rz = false; + for (InputDevice.MotionRange range : ranges) { + int axis = range.getAxis(); + if (axis == MotionEvent.AXIS_Z) { + have_z = true; + } else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) { + have_past_z_before_rz = true; + } + } + if (have_z && have_past_z_before_rz) { + // If both these exist, the compare() function changed sorting order. + // Set a bit to indicate this fact. + axis_mask |= 0x8000; + } + return axis_mask; + } + + int getButtonMask(InputDevice joystickDevice) { + int button_mask = 0; + int[] keys = new int[] { + KeyEvent.KEYCODE_BUTTON_A, + KeyEvent.KEYCODE_BUTTON_B, + KeyEvent.KEYCODE_BUTTON_X, + KeyEvent.KEYCODE_BUTTON_Y, + KeyEvent.KEYCODE_BACK, + KeyEvent.KEYCODE_MENU, + KeyEvent.KEYCODE_BUTTON_MODE, + KeyEvent.KEYCODE_BUTTON_START, + KeyEvent.KEYCODE_BUTTON_THUMBL, + KeyEvent.KEYCODE_BUTTON_THUMBR, + KeyEvent.KEYCODE_BUTTON_L1, + KeyEvent.KEYCODE_BUTTON_R1, + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_BUTTON_SELECT, + KeyEvent.KEYCODE_DPAD_CENTER, + + // These don't map into any SDL controller buttons directly + KeyEvent.KEYCODE_BUTTON_L2, + KeyEvent.KEYCODE_BUTTON_R2, + KeyEvent.KEYCODE_BUTTON_C, + KeyEvent.KEYCODE_BUTTON_Z, + KeyEvent.KEYCODE_BUTTON_1, + KeyEvent.KEYCODE_BUTTON_2, + KeyEvent.KEYCODE_BUTTON_3, + KeyEvent.KEYCODE_BUTTON_4, + KeyEvent.KEYCODE_BUTTON_5, + KeyEvent.KEYCODE_BUTTON_6, + KeyEvent.KEYCODE_BUTTON_7, + KeyEvent.KEYCODE_BUTTON_8, + KeyEvent.KEYCODE_BUTTON_9, + KeyEvent.KEYCODE_BUTTON_10, + KeyEvent.KEYCODE_BUTTON_11, + KeyEvent.KEYCODE_BUTTON_12, + KeyEvent.KEYCODE_BUTTON_13, + KeyEvent.KEYCODE_BUTTON_14, + KeyEvent.KEYCODE_BUTTON_15, + KeyEvent.KEYCODE_BUTTON_16, + }; + int[] masks = new int[] { + (1 << 0), // A -> A + (1 << 1), // B -> B + (1 << 2), // X -> X + (1 << 3), // Y -> Y + (1 << 4), // BACK -> BACK + (1 << 6), // MENU -> START + (1 << 5), // MODE -> GUIDE + (1 << 6), // START -> START + (1 << 7), // THUMBL -> LEFTSTICK + (1 << 8), // THUMBR -> RIGHTSTICK + (1 << 9), // L1 -> LEFTSHOULDER + (1 << 10), // R1 -> RIGHTSHOULDER + (1 << 11), // DPAD_UP -> DPAD_UP + (1 << 12), // DPAD_DOWN -> DPAD_DOWN + (1 << 13), // DPAD_LEFT -> DPAD_LEFT + (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT + (1 << 4), // SELECT -> BACK + (1 << 0), // DPAD_CENTER -> A + (1 << 15), // L2 -> ?? + (1 << 16), // R2 -> ?? + (1 << 17), // C -> ?? + (1 << 18), // Z -> ?? + (1 << 20), // 1 -> ?? + (1 << 21), // 2 -> ?? + (1 << 22), // 3 -> ?? + (1 << 23), // 4 -> ?? + (1 << 24), // 5 -> ?? + (1 << 25), // 6 -> ?? + (1 << 26), // 7 -> ?? + (1 << 27), // 8 -> ?? + (1 << 28), // 9 -> ?? + (1 << 29), // 10 -> ?? + (1 << 30), // 11 -> ?? + (1 << 31), // 12 -> ?? + // We're out of room... + 0xFFFFFFFF, // 13 -> ?? + 0xFFFFFFFF, // 14 -> ?? + 0xFFFFFFFF, // 15 -> ?? + 0xFFFFFFFF, // 16 -> ?? + }; + boolean[] has_keys = joystickDevice.hasKeys(keys); + for (int i = 0; i < keys.length; ++i) { + if (has_keys[i]) { + button_mask |= masks[i]; + } + } + return button_mask; + } + + void setLED(int device_id, int red, int green, int blue) { + if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) { + return; + } + SDLJoystick joystick = getJoystick(device_id); + if (joystick == null || joystick.lights.isEmpty()) { + return; + } + LightsRequest.Builder lightsRequest = new LightsRequest.Builder(); + LightState lightState = new LightState.Builder().setColor(Color.rgb(red, green, blue)).build(); + for (Light light : joystick.lights) { + if (light.hasRgbControl()) { + lightsRequest.addLight(light, lightState); + } + } + joystick.lightsSession.requestLights(lightsRequest.build()); + } + + void setSensorsEnabled(int device_id, boolean enabled) { + if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) { + return; + } + SDLJoystick joystick = getJoystick(device_id); + if (joystick == null || joystick.sensorManager == null) { + return; + } + if (enabled) { + if (joystick.accelerometerSensor != null) { + SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor, SensorManager.SENSOR_DELAY_GAME); + } + if (joystick.gyroscopeSensor != null) { + SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor, SensorManager.SENSOR_DELAY_GAME); + } + } else { + if (joystick.accelerometerSensor != null) { + SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor); + } + if (joystick.gyroscopeSensor != null) { + SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor); + } + } } } \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler_API16.java b/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler_API16.java deleted file mode 100644 index 71949447..00000000 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler_API16.java +++ /dev/null @@ -1,198 +0,0 @@ -package org.libsdl.app; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import android.content.Context; -import android.os.Build; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.util.Log; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; - -public class SDLJoystickHandler_API16 extends SDLJoystickHandler { - public static class SDLJoystick { - public int device_id; - public String name; - public String desc; - public ArrayList axes; - public ArrayList hats; - } - static class RangeComparator implements Comparator { - @Override - public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { - // Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL - int arg0Axis = arg0.getAxis(); - int arg1Axis = arg1.getAxis(); - if (arg0Axis == MotionEvent.AXIS_GAS) { - arg0Axis = MotionEvent.AXIS_BRAKE; - } else if (arg0Axis == MotionEvent.AXIS_BRAKE) { - arg0Axis = MotionEvent.AXIS_GAS; - } - if (arg1Axis == MotionEvent.AXIS_GAS) { - arg1Axis = MotionEvent.AXIS_BRAKE; - } else if (arg1Axis == MotionEvent.AXIS_BRAKE) { - arg1Axis = MotionEvent.AXIS_GAS; - } - - // Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ. - // This is because the usual pairing are: - // - AXIS_X + AXIS_Y (left stick). - // - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers). - // - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers). - // This sorts the axes in the above order, which tends to be correct - // for Xbox-ish game pads that have the right stick on RX/RY and the - // triggers on Z/RZ. - // - // Gamepads that don't have AXIS_Z/AXIS_RZ but use - // AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this. - // - // References: - // - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input - // - https://www.kernel.org/doc/html/latest/input/gamepad.html - if (arg0Axis == MotionEvent.AXIS_Z) { - arg0Axis = MotionEvent.AXIS_RZ - 1; - } else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) { - --arg0Axis; - } - if (arg1Axis == MotionEvent.AXIS_Z) { - arg1Axis = MotionEvent.AXIS_RZ - 1; - } else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) { - --arg1Axis; - } - - return arg0Axis - arg1Axis; - } - } - - private final ArrayList mJoysticks; - - public SDLJoystickHandler_API16() { - - mJoysticks = new ArrayList(); - } - - @Override - public void pollInputDevices() { - int[] deviceIds = InputDevice.getDeviceIds(); - - for (int device_id : deviceIds) { - if (SDLControllerManager.isDeviceSDLJoystick(device_id)) { - SDLJoystick joystick = getJoystick(device_id); - if (joystick == null) { - InputDevice joystickDevice = InputDevice.getDevice(device_id); - joystick = new SDLJoystick(); - joystick.device_id = device_id; - joystick.name = joystickDevice.getName(); - joystick.desc = getJoystickDescriptor(joystickDevice); - joystick.axes = new ArrayList(); - joystick.hats = new ArrayList(); - - List ranges = joystickDevice.getMotionRanges(); - Collections.sort(ranges, new RangeComparator()); - for (InputDevice.MotionRange range : ranges) { - if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { - if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { - joystick.hats.add(range); - } else { - joystick.axes.add(range); - } - } - } - - mJoysticks.add(joystick); - SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, - getVendorId(joystickDevice), getProductId(joystickDevice), false, - getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0); - } - } - } - - /* Check removed devices */ - ArrayList removedDevices = null; - for (SDLJoystick joystick : mJoysticks) { - int device_id = joystick.device_id; - int i; - for (i = 0; i < deviceIds.length; i++) { - if (device_id == deviceIds[i]) break; - } - if (i == deviceIds.length) { - if (removedDevices == null) { - removedDevices = new ArrayList(); - } - removedDevices.add(device_id); - } - } - - if (removedDevices != null) { - for (int device_id : removedDevices) { - SDLControllerManager.nativeRemoveJoystick(device_id); - for (int i = 0; i < mJoysticks.size(); i++) { - if (mJoysticks.get(i).device_id == device_id) { - mJoysticks.remove(i); - break; - } - } - } - } - } - - protected SDLJoystick getJoystick(int device_id) { - for (SDLJoystick joystick : mJoysticks) { - if (joystick.device_id == device_id) { - return joystick; - } - } - return null; - } - - @Override - public boolean handleMotionEvent(MotionEvent event) { - int actionPointerIndex = event.getActionIndex(); - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_MOVE) { - SDLJoystick joystick = getJoystick(event.getDeviceId()); - if (joystick != null) { - for (int i = 0; i < joystick.axes.size(); i++) { - InputDevice.MotionRange range = joystick.axes.get(i); - /* Normalize the value to -1...1 */ - float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f; - SDLControllerManager.onNativeJoy(joystick.device_id, i, value); - } - for (int i = 0; i < joystick.hats.size() / 2; i++) { - int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex)); - int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex)); - SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY); - } - } - } - return true; - } - - public String getJoystickDescriptor(InputDevice joystickDevice) { - String desc = joystickDevice.getDescriptor(); - - if (desc != null && !desc.isEmpty()) { - return desc; - } - - return joystickDevice.getName(); - } - public int getProductId(InputDevice joystickDevice) { - return 0; - } - public int getVendorId(InputDevice joystickDevice) { - return 0; - } - public int getAxisMask(List ranges) { - return -1; - } - public int getButtonMask(InputDevice joystickDevice) { - return -1; - } -} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler_API19.java b/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler_API19.java deleted file mode 100644 index dbf3553f..00000000 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLJoystickHandler_API19.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.libsdl.app; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import android.content.Context; -import android.os.Build; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.util.Log; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; - -public class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 { - - @Override - public int getProductId(InputDevice joystickDevice) { - return joystickDevice.getProductId(); - } - - @Override - public int getVendorId(InputDevice joystickDevice) { - return joystickDevice.getVendorId(); - } - - @Override - public int getAxisMask(List ranges) { - // For compatibility, keep computing the axis mask like before, - // only really distinguishing 2, 4 and 6 axes. - int axis_mask = 0; - if (ranges.size() >= 2) { - // ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY)) - axis_mask |= 0x0003; - } - if (ranges.size() >= 4) { - // ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY)) - axis_mask |= 0x000c; - } - if (ranges.size() >= 6) { - // ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) - axis_mask |= 0x0030; - } - // Also add an indicator bit for whether the sorting order has changed. - // This serves to disable outdated gamecontrollerdb.txt mappings. - boolean have_z = false; - boolean have_past_z_before_rz = false; - for (InputDevice.MotionRange range : ranges) { - int axis = range.getAxis(); - if (axis == MotionEvent.AXIS_Z) { - have_z = true; - } else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) { - have_past_z_before_rz = true; - } - } - if (have_z && have_past_z_before_rz) { - // If both these exist, the compare() function changed sorting order. - // Set a bit to indicate this fact. - axis_mask |= 0x8000; - } - return axis_mask; - } - - @Override - public int getButtonMask(InputDevice joystickDevice) { - int button_mask = 0; - int[] keys = new int[] { - KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_B, - KeyEvent.KEYCODE_BUTTON_X, - KeyEvent.KEYCODE_BUTTON_Y, - KeyEvent.KEYCODE_BACK, - KeyEvent.KEYCODE_MENU, - KeyEvent.KEYCODE_BUTTON_MODE, - KeyEvent.KEYCODE_BUTTON_START, - KeyEvent.KEYCODE_BUTTON_THUMBL, - KeyEvent.KEYCODE_BUTTON_THUMBR, - KeyEvent.KEYCODE_BUTTON_L1, - KeyEvent.KEYCODE_BUTTON_R1, - KeyEvent.KEYCODE_DPAD_UP, - KeyEvent.KEYCODE_DPAD_DOWN, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_BUTTON_SELECT, - KeyEvent.KEYCODE_DPAD_CENTER, - - // These don't map into any SDL controller buttons directly - KeyEvent.KEYCODE_BUTTON_L2, - KeyEvent.KEYCODE_BUTTON_R2, - KeyEvent.KEYCODE_BUTTON_C, - KeyEvent.KEYCODE_BUTTON_Z, - KeyEvent.KEYCODE_BUTTON_1, - KeyEvent.KEYCODE_BUTTON_2, - KeyEvent.KEYCODE_BUTTON_3, - KeyEvent.KEYCODE_BUTTON_4, - KeyEvent.KEYCODE_BUTTON_5, - KeyEvent.KEYCODE_BUTTON_6, - KeyEvent.KEYCODE_BUTTON_7, - KeyEvent.KEYCODE_BUTTON_8, - KeyEvent.KEYCODE_BUTTON_9, - KeyEvent.KEYCODE_BUTTON_10, - KeyEvent.KEYCODE_BUTTON_11, - KeyEvent.KEYCODE_BUTTON_12, - KeyEvent.KEYCODE_BUTTON_13, - KeyEvent.KEYCODE_BUTTON_14, - KeyEvent.KEYCODE_BUTTON_15, - KeyEvent.KEYCODE_BUTTON_16, - }; - int[] masks = new int[] { - (1 << 0), // A -> A - (1 << 1), // B -> B - (1 << 2), // X -> X - (1 << 3), // Y -> Y - (1 << 4), // BACK -> BACK - (1 << 6), // MENU -> START - (1 << 5), // MODE -> GUIDE - (1 << 6), // START -> START - (1 << 7), // THUMBL -> LEFTSTICK - (1 << 8), // THUMBR -> RIGHTSTICK - (1 << 9), // L1 -> LEFTSHOULDER - (1 << 10), // R1 -> RIGHTSHOULDER - (1 << 11), // DPAD_UP -> DPAD_UP - (1 << 12), // DPAD_DOWN -> DPAD_DOWN - (1 << 13), // DPAD_LEFT -> DPAD_LEFT - (1 << 14), // DPAD_RIGHT -> DPAD_RIGHT - (1 << 4), // SELECT -> BACK - (1 << 0), // DPAD_CENTER -> A - (1 << 15), // L2 -> ?? - (1 << 16), // R2 -> ?? - (1 << 17), // C -> ?? - (1 << 18), // Z -> ?? - (1 << 20), // 1 -> ?? - (1 << 21), // 2 -> ?? - (1 << 22), // 3 -> ?? - (1 << 23), // 4 -> ?? - (1 << 24), // 5 -> ?? - (1 << 25), // 6 -> ?? - (1 << 26), // 7 -> ?? - (1 << 27), // 8 -> ?? - (1 << 28), // 9 -> ?? - (1 << 29), // 10 -> ?? - (1 << 30), // 11 -> ?? - (1 << 31), // 12 -> ?? - // We're out of room... - 0xFFFFFFFF, // 13 -> ?? - 0xFFFFFFFF, // 14 -> ?? - 0xFFFFFFFF, // 15 -> ?? - 0xFFFFFFFF, // 16 -> ?? - }; - boolean[] has_keys = joystickDevice.hasKeys(keys); - for (int i = 0; i < keys.length; ++i) { - if (has_keys[i]) { - button_mask |= masks[i]; - } - } - return button_mask; - } -} \ No newline at end of file diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLSensorManager.java b/Knossos.NET.Android/java/org/libsdl/app/SDLSensorManager.java new file mode 100644 index 00000000..4ed4f012 --- /dev/null +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLSensorManager.java @@ -0,0 +1,73 @@ +package org.libsdl.app; + +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +// This class coordinates synchronized access to sensor manager registration +// +// This prevents a java.util.ConcurrentModificationException exception on +// Android 16, specifically on the Samsung Tab S9 Ultra. + +class SDLSensorManager +{ + static private SDLSensorManager mManager = new SDLSensorManager(); + + static final int RETRY_COUNT = 3; + + public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) { + mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs); + } + + public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) { + mManager.UnregisterListener(manager, listener, sensor); + } + + private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) { + int retries = 0; + boolean complete = false; + while (!complete) { + try { + manager.registerListener(listener, sensor, samplingPeriodUs, null); + complete = true; + } catch (java.util.ConcurrentModificationException e) { + ++retries; + if (retries <= RETRY_COUNT) { + // Sleep a bit and try again + try { + Thread.sleep(1); + } catch (Exception e2) { + } + } else { + Log.v("SDL", "Multiple ConcurrentModificationException caught while registering sensor listener, canceling operation"); + complete = true; + } + } + } + } + + private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) { + int retries = 0; + boolean complete = false; + while (!complete) { + try { + manager.unregisterListener(listener, sensor); + complete = true; + } catch (java.util.ConcurrentModificationException e) { + ++retries; + if (retries <= RETRY_COUNT) { + // Sleep a bit and try again + try { + Thread.sleep(1); + } catch (Exception e2) { + } + } else { + Log.v("SDL", "Multiple ConcurrentModificationException caught while unregistering sensor listener, canceling operation"); + complete = true; + } + } + } + } +} + diff --git a/Knossos.NET.Android/java/org/libsdl/app/SDLSurface.java b/Knossos.NET.Android/java/org/libsdl/app/SDLSurface.java index 0857e4b6..e28870fa 100644 --- a/Knossos.NET.Android/java/org/libsdl/app/SDLSurface.java +++ b/Knossos.NET.Android/java/org/libsdl/app/SDLSurface.java @@ -3,6 +3,7 @@ import android.content.Context; import android.content.pm.ActivityInfo; +import android.graphics.Insets; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -14,12 +15,15 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.PointerIcon; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.ScaleGestureDetector; /** SDLSurface. This is what we draw on, so we need to know when it's created @@ -28,7 +32,8 @@ 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 { + View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, + SensorEventListener, ScaleGestureDetector.OnScaleGestureListener { // Sensors protected SensorManager mSensorManager; @@ -38,16 +43,25 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, protected float mWidth, mHeight; // Is SurfaceView ready for rendering - public boolean mIsSurfaceReady; + protected boolean mIsSurfaceReady; + + // Is on-screen keyboard visible + protected boolean mKeyboardVisible; + + // Pinch events + private final ScaleGestureDetector scaleGestureDetector; // Startup - public SDLSurface(Context context) { + protected SDLSurface(Context context) { super(context); getHolder().addCallback(this); + scaleGestureDetector = new ScaleGestureDetector(context, this); + setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + setOnApplyWindowInsetsListener(this); setOnKeyListener(this); setOnTouchListener(this); @@ -63,20 +77,21 @@ public SDLSurface(Context context) { mIsSurfaceReady = false; } - public void handlePause() { + protected void handlePause() { enableSensor(Sensor.TYPE_ACCELEROMETER, false); } - public void handleResume() { + protected void handleResume() { setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + setOnApplyWindowInsetsListener(this); setOnKeyListener(this); setOnTouchListener(this); enableSensor(Sensor.TYPE_ACCELEROMETER, true); } - public Surface getNativeSurface() { + protected Surface getNativeSurface() { return getHolder().getSurface(); } @@ -114,14 +129,15 @@ public void surfaceChanged(SurfaceHolder holder, mHeight = height; int nDeviceWidth = width; int nDeviceHeight = height; + float density = 1.0f; 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; - } + DisplayMetrics realMetrics = new DisplayMetrics(); + mDisplay.getRealMetrics( realMetrics ); + nDeviceWidth = realMetrics.widthPixels; + nDeviceHeight = realMetrics.heightPixels; + // Use densityDpi instead of density to more closely match what the UI scale is + density = (float)realMetrics.densityDpi / 160.0f; } catch(Exception ignored) { } @@ -132,7 +148,7 @@ public void surfaceChanged(SurfaceHolder holder, 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.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate()); SDLActivity.onNativeResize(); // Prevent a screen distortion glitch, @@ -161,13 +177,10 @@ public void surfaceChanged(SurfaceHolder holder, } } - // Don't skip in MultiWindow. + // Don't skip if we might be multi-window or have popup dialogs 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; - } + skip = false; } } @@ -187,12 +200,59 @@ public void surfaceChanged(SurfaceHolder holder, SDLActivity.handleNativeState(); } + // Window inset + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) { + Insets combined = insets.getInsets(WindowInsets.Type.systemBars() | + WindowInsets.Type.systemGestures() | + WindowInsets.Type.mandatorySystemGestures() | + WindowInsets.Type.tappableElement() | + WindowInsets.Type.displayCutout()); + + SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom); + + if (insets.isVisible(WindowInsets.Type.ime())) { + if (!mKeyboardVisible) { + mKeyboardVisible = true; + SDLActivity.onNativeScreenKeyboardShown(); + } + } else { + if (mKeyboardVisible) { + mKeyboardVisible = false; + SDLActivity.onNativeScreenKeyboardHidden(); + } + } + } + + // Pass these to any child views in case they need them + return insets; + } + // Key events @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return SDLActivity.handleKeyEvent(v, keyCode, event, null); } + private float getNormalizedX(float x) + { + if (mWidth <= 1) { + return 0.5f; + } else { + return (x / (mWidth - 1)); + } + } + + private float getNormalizedY(float y) + { + if (mHeight <= 1) { + return 0.5f; + } else { + return (y / (mHeight - 1)); + } + } + // Touch events @Override public boolean onTouch(View v, MotionEvent event) { @@ -200,113 +260,79 @@ public boolean onTouch(View v, MotionEvent event) { int touchDevId = event.getDeviceId(); final int pointerCount = event.getPointerCount(); int action = event.getActionMasked(); - int pointerFingerId; - int i = -1; + int pointerId; + int i = 0; 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; + if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) + i = event.getActionIndex(); + + do { + int toolType = event.getToolType(i); + + if (toolType == MotionEvent.TOOL_TYPE_MOUSE) { + int buttonState = event.getButtonState(); + boolean relative = false; + + // 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_API14 motionListener = SDLActivity.getMotionListener(); + x = motionListener.getEventX(event, i); + y = motionListener.getEventY(event, i); + relative = motionListener.inRelativeMode(); + + SDLActivity.onNativeMouse(buttonState, action, x, y, relative); + } else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) { + pointerId = event.getPointerId(i); + x = event.getX(i); + y = event.getY(i); + 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; } - } 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); + // BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP + int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30)); + if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) { + buttonState |= 0x08; + } - 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; + SDLActivity.onNativePen(pointerId, SDLActivity.getMotionListener().getPenDeviceType(event.getDevice()), buttonState, action, x, y, p); + } else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN + pointerId = event.getPointerId(i); + x = getNormalizedX(event.getX(i)); + y = getNormalizedY(event.getY(i)); + 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; + } - 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; + SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p); + } - 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; + // Non-primary up/down + if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) + break; + } while (++i < pointerCount); - default: - break; - } - } + scaleGestureDetector.onTouchEvent(event); return true; - } + } // Sensor events - public void enableSensor(int sensortype, boolean enabled) { + protected void enableSensor(int sensortype, boolean enabled) { // TODO: This uses getDefaultSensor - what if we have >1 accels? if (enabled) { - mSensorManager.registerListener(this, + SDLSensorManager.registerListener(mSensorManager, this, mSensorManager.getDefaultSensor(sensortype), - SensorManager.SENSOR_DELAY_GAME, null); + SensorManager.SENSOR_DELAY_GAME); } else { - mSensorManager.unregisterListener(this, + SDLSensorManager.unregisterListener(mSensorManager, this, mSensorManager.getDefaultSensor(sensortype)); } } @@ -322,36 +348,36 @@ public void onSensorChanged(SensorEvent event) { // Since we may have an orientation set, we won't receive onConfigurationChanged events. // We thus should check here. - int newOrientation; + int newRotation; float x, y; switch (mDisplay.getRotation()) { + case Surface.ROTATION_0: + default: + x = event.values[0]; + y = event.values[1]; + newRotation = 0; + break; 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; + newRotation = 90; break; case Surface.ROTATION_180: x = -event.values[0]; y = -event.values[1]; - newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED; + newRotation = 180; break; - case Surface.ROTATION_0: - default: - x = event.values[0]; - y = event.values[1]; - newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT; + case Surface.ROTATION_270: + x = event.values[1]; + y = -event.values[0]; + newRotation = 270; break; } - if (newOrientation != SDLActivity.mCurrentOrientation) { - SDLActivity.mCurrentOrientation = newOrientation; - SDLActivity.onNativeOrientationChanged(newOrientation); + if (newRotation != SDLActivity.mCurrentRotation) { + SDLActivity.mCurrentRotation = newRotation; + SDLActivity.onNativeRotationChanged(newRotation); } SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH, @@ -362,44 +388,39 @@ public void onSensorChanged(SensorEvent event) { } } + // Prevent android internal NullPointerException (https://github.com/libsdl-org/SDL/issues/13306) + @Override + public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { + try { + return super.onResolvePointerIcon(event, pointerIndex); + } catch (NullPointerException e) { + return null; + } + } + // Captured pointer events for API 26. + @Override 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; - } + return SDLActivity.getMotionListener().onGenericMotion(this, event); + } - x = event.getX(0); - y = event.getY(0); - int button = event.getButtonState(); + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scale = detector.getScaleFactor(); + SDLActivity.onNativePinchUpdate(scale); + return true; + } - SDLActivity.onNativeMouse(button, action, x, y, true); - return true; - } + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + SDLActivity.onNativePinchStart(); + return true; + } - return false; + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + SDLActivity.onNativePinchEnd(); } + } diff --git a/Knossos.NET.Android/natives/arm64-v8a/libtouchcontrols.so b/Knossos.NET.Android/natives/arm64-v8a/libtouchcontrols.so index 8033be3a..73b07e79 100644 Binary files a/Knossos.NET.Android/natives/arm64-v8a/libtouchcontrols.so and b/Knossos.NET.Android/natives/arm64-v8a/libtouchcontrols.so differ diff --git a/Knossos.NET.Android/natives/armeabi-v7a/libtouchcontrols.so b/Knossos.NET.Android/natives/armeabi-v7a/libtouchcontrols.so index ff97a3ed..8a9520cb 100644 Binary files a/Knossos.NET.Android/natives/armeabi-v7a/libtouchcontrols.so and b/Knossos.NET.Android/natives/armeabi-v7a/libtouchcontrols.so differ diff --git a/Knossos.NET.Android/natives/x86/libtouchcontrols.so b/Knossos.NET.Android/natives/x86/libtouchcontrols.so index daa64e3c..c2cde025 100644 Binary files a/Knossos.NET.Android/natives/x86/libtouchcontrols.so and b/Knossos.NET.Android/natives/x86/libtouchcontrols.so differ diff --git a/Knossos.NET.Android/natives/x86_64/libtouchcontrols.so b/Knossos.NET.Android/natives/x86_64/libtouchcontrols.so index 34988664..a253fa42 100644 Binary files a/Knossos.NET.Android/natives/x86_64/libtouchcontrols.so and b/Knossos.NET.Android/natives/x86_64/libtouchcontrols.so differ diff --git a/Knossos.NET/Classes/AndroidHelper.cs b/Knossos.NET/Classes/AndroidHelper.cs index 6413df55..cb888498 100644 --- a/Knossos.NET/Classes/AndroidHelper.cs +++ b/Knossos.NET/Classes/AndroidHelper.cs @@ -192,7 +192,7 @@ public static void LaunchFSO(string engineLibPath, string? workingFolder, string public static string GetDefaultKnetDir() => ""; public static string GetDefaultKnetDataDir() => ""; public static string GetDefaultFSODataDir() => ""; - public static void LaunchFSO(string engineLibPath, string? workingFolder, string cmdline) { } + public static void LaunchFSO(string engineLibPath, string? workingFolder, string cmdline) { } #endif }