Browse Source

Merge branch 'latest-sdl' into dev

dm 7 years ago
parent
commit
169d05f0ae

+ 19 - 0
oxygine/SDL/android/lib/src/org/libsdl/app/HIDDevice.java

@@ -0,0 +1,19 @@
+package org.libsdl.app;
+
+interface HIDDevice
+{
+    public int getId();
+    public int getVendorId();
+    public int getProductId();
+    public String getSerialNumber();
+    public int getVersion();
+    public String getManufacturerName();
+    public String getProductName();
+    public boolean open();
+    public int sendFeatureReport(byte[] report);
+    public int sendOutputReport(byte[] report);
+    public boolean getFeatureReport(byte[] report);
+    public void setFrozen(boolean frozen);
+    public void close();
+    public void shutdown();
+}

+ 642 - 0
oxygine/SDL/android/lib/src/org/libsdl/app/HIDDeviceBLESteamController.java

@@ -0,0 +1,642 @@
+package org.libsdl.app;
+
+import android.content.Context;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothGattService;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+//import com.android.internal.util.HexDump;
+
+import java.lang.Runnable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.UUID;
+
+class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
+
+    private static final String TAG = "hidapi";
+    private HIDDeviceManager mManager;
+    private BluetoothDevice mDevice;
+    private int mDeviceId;
+    private BluetoothGatt mGatt;
+    private boolean mIsRegistered = false;
+    private boolean mIsConnected = false;
+    private boolean mIsChromebook = false;
+    private boolean mIsReconnecting = false;
+    private boolean mFrozen = false;
+    private LinkedList<GattOperation> mOperations;
+    GattOperation mCurrentOperation = null;
+    private Handler mHandler;
+
+    private static final int TRANSPORT_AUTO = 0;
+    private static final int TRANSPORT_BREDR = 1;
+    private static final int TRANSPORT_LE = 2;
+
+    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 private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
+
+    static class GattOperation {
+        private enum Operation {
+            CHR_READ,
+            CHR_WRITE,
+            ENABLE_NOTIFICATION
+        }
+
+        Operation mOp;
+        UUID mUuid;
+        byte[] mValue;
+        BluetoothGatt mGatt;
+        boolean mResult = true;
+
+        private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
+            mGatt = gatt;
+            mOp = operation;
+            mUuid = uuid;
+        }
+
+        private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
+            mGatt = gatt;
+            mOp = operation;
+            mUuid = uuid;
+            mValue = value;
+        }
+
+        public void run() {
+            // This is executed in main thread
+            BluetoothGattCharacteristic chr;
+
+            switch (mOp) {
+                case CHR_READ:
+                    chr = getCharacteristic(mUuid);
+                    //Log.v(TAG, "Reading characteristic " + chr.getUuid());
+                    if (!mGatt.readCharacteristic(chr)) {
+                        Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
+                        mResult = false;
+                        break;
+                    }
+                    mResult = true;
+                    break;
+                case CHR_WRITE:
+                    chr = getCharacteristic(mUuid);
+                    //Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
+                    chr.setValue(mValue);
+                    if (!mGatt.writeCharacteristic(chr)) {
+                        Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
+                        mResult = false;
+                        break;
+                    }
+                    mResult = true;
+                    break;
+                case ENABLE_NOTIFICATION:
+                    chr = getCharacteristic(mUuid);
+                    //Log.v(TAG, "Writing descriptor of " + chr.getUuid());
+                    if (chr != null) {
+                        BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
+                        if (cccd != null) {
+                            int properties = chr.getProperties();
+                            byte[] value;
+                            if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
+                                value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
+                            } else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
+                                value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
+                            } else {
+                                Log.e(TAG, "Unable to start notifications on input characteristic");
+                                mResult = false;
+                                return;
+                            }
+
+                            mGatt.setCharacteristicNotification(chr, true);
+                            cccd.setValue(value);
+                            if (!mGatt.writeDescriptor(cccd)) {
+                                Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
+                                mResult = false;
+                                return;
+                            }
+                            mResult = true;
+                        }
+                    }
+            }
+        }
+
+        public boolean finish() {
+            return mResult;
+        }
+
+        private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
+            BluetoothGattService valveService = mGatt.getService(steamControllerService);
+            if (valveService == null)
+                return null;
+            return valveService.getCharacteristic(uuid);
+        }
+
+        static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
+            return new GattOperation(gatt, Operation.CHR_READ, uuid);
+        }
+
+        static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
+            return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
+        }
+
+        static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
+            return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
+        }
+    }
+
+    public 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");
+        mOperations = new LinkedList<GattOperation>();
+        mHandler = new Handler(Looper.getMainLooper());
+
+        mGatt = connectGatt();
+        final HIDDeviceBLESteamController finalThis = this;
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                finalThis.checkConnectionForChromebookIssue();
+            }
+        }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
+    }
+
+    public String getIdentifier() {
+        return String.format("SteamController.%s", mDevice.getAddress());
+    }
+
+    public BluetoothGatt getGatt() {
+        return mGatt;
+    }
+
+    // Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
+    // of TRANSPORT_LE.  Let's force ourselves to connect low energy.
+    private BluetoothGatt connectGatt(boolean managed) {
+        try {
+            Method m = mDevice.getClass().getDeclaredMethod("connectGatt", Context.class, boolean.class, BluetoothGattCallback.class, int.class);
+            return (BluetoothGatt) m.invoke(mDevice, mManager.getContext(), managed, this, TRANSPORT_LE);
+        } catch (Exception e) {
+            return mDevice.connectGatt(mManager.getContext(), managed, this);
+        }
+    }
+
+    private BluetoothGatt connectGatt() {
+        return connectGatt(false);
+    }
+
+    protected int getConnectionState() {
+
+        Context context = mManager.getContext();
+        if (context == null) {
+            // We are lacking any context to get our Bluetooth information.  We'll just assume disconnected.
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+
+        BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
+        if (btManager == null) {
+            // This device doesn't support Bluetooth.  We should never be here, because how did
+            // we instantiate a device to start with?
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+
+        return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
+    }
+
+    public void reconnect() {
+
+        if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
+            mGatt.disconnect();
+            mGatt = connectGatt();
+        }
+
+    }
+
+    protected void checkConnectionForChromebookIssue() {
+        if (!mIsChromebook) {
+            // We only do this on Chromebooks, because otherwise it's really annoying to just attempt
+            // over and over.
+            return;
+        }
+
+        int connectionState = getConnectionState();
+
+        switch (connectionState) {
+            case BluetoothProfile.STATE_CONNECTED:
+                if (!mIsConnected) {
+                    // We are in the Bad Chromebook Place.  We can force a disconnect
+                    // to try to recover.
+                    Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback.  Forcing a reconnect.");
+                    mIsReconnecting = true;
+                    mGatt.disconnect();
+                    mGatt = connectGatt(false);
+                    break;
+                }
+                else if (!isRegistered()) {
+                    if (mGatt.getServices().size() > 0) {
+                        Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration.  Trying to recover.");
+                        probeService(this);
+                    }
+                    else {
+                        Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services.  Trying to recover.");
+                        mIsReconnecting = true;
+                        mGatt.disconnect();
+                        mGatt = connectGatt(false);
+                        break;
+                    }
+                }
+                else {
+                    Log.v(TAG, "Chromebook: We are connected, and registered.  Everything's good!");
+                    return;
+                }
+                break;
+
+            case BluetoothProfile.STATE_DISCONNECTED:
+                Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us.  Attempting a disconnect/reconnect, but we may not be able to recover.");
+
+                mIsReconnecting = true;
+                mGatt.disconnect();
+                mGatt = connectGatt(false);
+                break;
+
+            case BluetoothProfile.STATE_CONNECTING:
+                Log.v(TAG, "Chromebook: We're still trying to connect.  Waiting a bit longer.");
+                break;
+        }
+
+        final HIDDeviceBLESteamController finalThis = this;
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                finalThis.checkConnectionForChromebookIssue();
+            }
+        }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
+    }
+
+    private boolean isRegistered() {
+        return mIsRegistered;
+    }
+
+    private void setRegistered() {
+        mIsRegistered = true;
+    }
+
+    private boolean probeService(HIDDeviceBLESteamController controller) {
+
+        if (isRegistered()) {
+            return true;
+        }
+
+        if (!mIsConnected) {
+            return false;
+        }
+
+        Log.v(TAG, "probeService controller=" + controller);
+
+        for (BluetoothGattService service : mGatt.getServices()) {
+            if (service.getUuid().equals(steamControllerService)) {
+                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");
+                        // Start notifications
+                        BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
+                        if (cccd != null) {
+                            enableNotification(chr.getUuid());
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+
+        if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
+            Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
+            mIsConnected = false;
+            mIsReconnecting = true;
+            mGatt.disconnect();
+            mGatt = connectGatt(false);
+        }
+
+        return false;
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void finishCurrentGattOperation() {
+        GattOperation op = null;
+        synchronized (mOperations) {
+            if (mCurrentOperation != null) {
+                op = mCurrentOperation;
+                mCurrentOperation = null;
+            }
+        }
+        if (op != null) {
+            boolean result = op.finish(); // TODO: Maybe in main thread as well?
+
+            // Our operation failed, let's add it back to the beginning of our queue.
+            if (!result) {
+                mOperations.addFirst(op);
+            }
+        }
+        executeNextGattOperation();
+    }
+
+    private void executeNextGattOperation() {
+        synchronized (mOperations) {
+            if (mCurrentOperation != null)
+                return;
+
+            if (mOperations.isEmpty())
+                return;
+
+            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;
+                    }
+
+                    mCurrentOperation.run();
+                    // now wait for the GATT callback and when it comes, finish this operation
+                }
+            }
+        });
+    }
+
+    private void queueGattOperation(GattOperation op) {
+        synchronized (mOperations) {
+            mOperations.add(op);
+        }
+        executeNextGattOperation();
+    }
+
+    private void enableNotification(UUID chrUuid) {
+        GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
+        queueGattOperation(op);
+    }
+
+    public void writeCharacteristic(UUID uuid, byte[] value) {
+        GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
+        queueGattOperation(op);
+    }
+
+    public void readCharacteristic(UUID uuid) {
+        GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
+        queueGattOperation(op);
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    //////////////  BluetoothGattCallback overridden methods
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
+        //Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
+        mIsReconnecting = false;
+        if (newState == 2) {
+            mIsConnected = true;
+            // Run directly, without GattOperation
+            if (!isRegistered()) {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mGatt.discoverServices();
+                    }
+                });
+            }
+        } 
+        else if (newState == 0) {
+            mIsConnected = false;
+        }
+
+        // Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
+    }
+
+    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+        //Log.v(TAG, "onServicesDiscovered status=" + status);
+        if (status == 0) {
+            if (gatt.getServices().size() == 0) {
+                Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
+                mIsReconnecting = true;
+                mIsConnected = false;
+                gatt.disconnect();
+                mGatt = connectGatt(false);
+            }
+            else {
+                probeService(this);
+            }
+        }
+    }
+
+    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());
+        }
+
+        finishCurrentGattOperation();
+    }
+
+    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+        //Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
+
+        if (characteristic.getUuid().equals(reportCharacteristic)) {
+            // 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);
+                setRegistered();
+            }
+        }
+
+        finishCurrentGattOperation();
+    }
+
+    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) {
+            mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
+        }
+    }
+
+    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());
+
+        if (chr.getUuid().equals(inputCharacteristic)) {
+            boolean hasWrittenInputDescriptor = true;
+            BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
+            if (reportChr != null) {
+                Log.v(TAG, "Writing report characteristic to enter valve mode");
+                reportChr.setValue(enterValveMode);
+                gatt.writeCharacteristic(reportChr);
+            }
+        }
+
+        finishCurrentGattOperation();
+    }
+
+    public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+        //Log.v(TAG, "onReliableWriteCompleted status=" + status);
+    }
+
+    public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+        //Log.v(TAG, "onReadRemoteRssi status=" + status);
+    }
+
+    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+        //Log.v(TAG, "onMtuChanged status=" + status);
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    //////// Public API
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public int getId() {
+        return mDeviceId;
+    }
+
+    @Override
+    public int getVendorId() {
+        // Valve Corporation
+        final int VALVE_USB_VID = 0x28DE;
+        return VALVE_USB_VID;
+    }
+
+    @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;
+    }
+
+    @Override
+    public String getSerialNumber() {
+        // This will be read later via feature report by Steam
+        return "12345";
+    }
+
+    @Override
+    public int getVersion() {
+        return 0;
+    }
+
+    @Override
+    public String getManufacturerName() {
+        return "Valve Corporation";
+    }
+
+    @Override
+    public String getProductName() {
+        return "Steam Controller";
+    }
+
+    @Override
+    public boolean open() {
+        return true;
+    }
+
+    @Override
+    public int sendFeatureReport(byte[] report) {
+        if (!isRegistered()) {
+            Log.e(TAG, "Attempted sendFeatureReport 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;
+    }
+
+    @Override
+    public int sendOutputReport(byte[] report) {
+        if (!isRegistered()) {
+            Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
+            if (mIsConnected) {
+                probeService(this);
+            }
+            return -1;
+        }
+
+        //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
+        writeCharacteristic(reportCharacteristic, report);
+        return report.length;
+    }
+
+    @Override
+    public boolean getFeatureReport(byte[] report) {
+        if (!isRegistered()) {
+            Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
+            if (mIsConnected) {
+                probeService(this);
+            }
+            return false;
+        }
+
+        //Log.v(TAG, "getFeatureReport");
+        readCharacteristic(reportCharacteristic);
+        return true;
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void setFrozen(boolean frozen) {
+        mFrozen = frozen;
+    }
+
+    @Override
+    public void shutdown() {
+        close();
+
+        BluetoothGatt g = mGatt;
+        if (g != null) {
+            g.disconnect();
+            g.close();
+            mGatt = null;
+        }
+        mManager = null;
+        mIsRegistered = false;
+        mIsConnected = false;
+        mOperations.clear();
+    }
+
+}
+

+ 657 - 0
oxygine/SDL/android/lib/src/org/libsdl/app/HIDDeviceManager.java

@@ -0,0 +1,657 @@
+package org.libsdl.app;
+
+import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.util.Log;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.hardware.usb.*;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
+
+public class HIDDeviceManager {
+    private static final String TAG = "hidapi";
+    private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
+
+    private static HIDDeviceManager sManager;
+    private static int sManagerRefCount = 0;
+
+    public static HIDDeviceManager acquire(Context context) {
+        if (sManagerRefCount == 0) {
+            sManager = new HIDDeviceManager(context);
+        }
+        ++sManagerRefCount;
+        return sManager;
+    }
+
+    public static void release(HIDDeviceManager manager) {
+        if (manager == sManager) {
+            --sManagerRefCount;
+            if (sManagerRefCount == 0) {
+                sManager.close();
+                sManager = null;
+            }
+        }
+    }
+
+    private Context mContext;
+    private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
+    private HashMap<UsbDevice, HIDDeviceUSB> mUSBDevices = new HashMap<UsbDevice, HIDDeviceUSB>();
+    private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
+    private int mNextDeviceId = 0;
+    private SharedPreferences mSharedPreferences = null;
+    private boolean mIsChromebook = false;
+    private UsbManager mUsbManager;
+    private Handler mHandler;
+    private BluetoothManager mBluetoothManager;
+    private List<BluetoothDevice> mLastBluetoothDevices;
+
+    private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
+                UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+                handleUsbDeviceAttached(usbDevice);
+            } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
+                UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+                handleUsbDeviceDetached(usbDevice);
+            } else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
+                UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
+                handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
+            }
+        }
+    };
+
+    private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            // Bluetooth device was connected. If it was a Steam Controller, handle it
+            if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                Log.d(TAG, "Bluetooth device connected: " + device);
+
+                if (isSteamController(device)) {
+                    connectBluetoothDevice(device);
+                }
+            }
+
+            // Bluetooth device was disconnected, remove from controller manager (if any)
+            if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
+                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+                Log.d(TAG, "Bluetooth device disconnected: " + device);
+
+                disconnectBluetoothDevice(device);
+            }
+        }
+    };
+
+    private HIDDeviceManager(Context context) {
+        mContext = context;
+
+        // Make sure we have the HIDAPI library loaded with the native functions
+        try {
+            SDL.loadLibrary("hidapi");
+        } catch (Exception e) {
+            Log.w(TAG, "Couldn't load hidapi: " + e.toString());
+            return;
+        }
+
+        HIDDeviceRegisterCallback();
+
+        mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
+        mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
+
+//        if (shouldClear) {
+//            SharedPreferences.Editor spedit = mSharedPreferences.edit();
+//            spedit.clear();
+//            spedit.commit();
+//        }
+//        else
+        {
+            mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
+        }
+
+        initializeUSB();
+        initializeBluetooth();
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public int getDeviceIDForIdentifier(String identifier) {
+        SharedPreferences.Editor spedit = mSharedPreferences.edit();
+
+        int result = mSharedPreferences.getInt(identifier, 0);
+        if (result == 0) {
+            result = mNextDeviceId++;
+            spedit.putInt("next_device_id", mNextDeviceId);
+        }
+
+        spedit.putInt(identifier, result);
+        spedit.commit();
+        return result;
+    }
+
+    private void initializeUSB() {
+        mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
+
+        /*
+        // Logging
+        for (UsbDevice device : mUsbManager.getDeviceList().values()) {
+            Log.i(TAG,"Path: " + device.getDeviceName());
+            Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
+            Log.i(TAG,"Product: " + device.getProductName());
+            Log.i(TAG,"ID: " + device.getDeviceId());
+            Log.i(TAG,"Class: " + device.getDeviceClass());
+            Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
+            Log.i(TAG,"Vendor ID " + device.getVendorId());
+            Log.i(TAG,"Product ID: " + device.getProductId());
+            Log.i(TAG,"Interface count: " + device.getInterfaceCount());
+            Log.i(TAG,"---------------------------------------");
+
+            // Get interface details
+            for (int index = 0; index < device.getInterfaceCount(); index++) {
+                UsbInterface mUsbInterface = device.getInterface(index);
+                Log.i(TAG,"  *****     *****");
+                Log.i(TAG,"  Interface index: " + index);
+                Log.i(TAG,"  Interface ID: " + mUsbInterface.getId());
+                Log.i(TAG,"  Interface class: " + mUsbInterface.getInterfaceClass());
+                Log.i(TAG,"  Interface subclass: " + mUsbInterface.getInterfaceSubclass());
+                Log.i(TAG,"  Interface protocol: " + mUsbInterface.getInterfaceProtocol());
+                Log.i(TAG,"  Endpoint count: " + mUsbInterface.getEndpointCount());
+
+                // Get endpoint details 
+                for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
+                {
+                    UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
+                    Log.i(TAG,"    ++++   ++++   ++++");
+                    Log.i(TAG,"    Endpoint index: " + epi);
+                    Log.i(TAG,"    Attributes: " + mEndpoint.getAttributes());
+                    Log.i(TAG,"    Direction: " + mEndpoint.getDirection());
+                    Log.i(TAG,"    Number: " + mEndpoint.getEndpointNumber());
+                    Log.i(TAG,"    Interval: " + mEndpoint.getInterval());
+                    Log.i(TAG,"    Packet size: " + mEndpoint.getMaxPacketSize());
+                    Log.i(TAG,"    Type: " + mEndpoint.getType());
+                }
+            }
+        }
+        Log.i(TAG," No more devices connected.");
+        */
+
+        // Register for USB broadcasts and permission completions
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+        filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
+        mContext.registerReceiver(mUsbBroadcast, filter);
+
+        for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
+            handleUsbDeviceAttached(usbDevice);
+        }
+    }
+
+    UsbManager getUSBManager() {
+        return mUsbManager;
+    }
+
+    private void shutdownUSB() {
+        try {
+            mContext.unregisterReceiver(mUsbBroadcast);
+        } catch (Exception e) {
+            // We may not have registered, that's okay
+        }
+    }
+
+    private boolean isHIDDeviceUSB(UsbDevice usbDevice) {
+        for (int interface_number = 0; interface_number < usbDevice.getInterfaceCount(); ++interface_number) {
+            if (isHIDDeviceInterface(usbDevice, interface_number)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isHIDDeviceInterface(UsbDevice usbDevice, int interface_number) {
+        UsbInterface usbInterface = usbDevice.getInterface(interface_number);
+        if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
+            return true;
+        }
+        if (interface_number == 0) {
+            if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
+        final int XB360_IFACE_SUBCLASS = 93;
+        final int XB360_IFACE_PROTOCOL = 1; // Wired only
+        final int[] SUPPORTED_VENDORS = {
+            0x0079, // GPD Win 2
+            0x044f, // Thrustmaster
+            0x045e, // Microsoft
+            0x046d, // Logitech
+            0x056e, // Elecom
+            0x06a3, // Saitek
+            0x0738, // Mad Catz
+            0x07ff, // Mad Catz
+            0x0e6f, // Unknown
+            0x0f0d, // Hori
+            0x11c9, // Nacon
+            0x12ab, // Unknown
+            0x1430, // RedOctane
+            0x146b, // BigBen
+            0x1532, // Razer Sabertooth
+            0x15e4, // Numark
+            0x162e, // Joytech
+            0x1689, // Razer Onza
+            0x1bad, // Harmonix
+            0x24c6, // PowerA
+        };
+
+        if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
+            usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
+            usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
+            int vendor_id = usbDevice.getVendorId();
+            for (int supportedVid : SUPPORTED_VENDORS) {
+                if (vendor_id == supportedVid) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
+        final int XB1_IFACE_SUBCLASS = 71;
+        final int XB1_IFACE_PROTOCOL = 208;
+        final int[] SUPPORTED_VENDORS = {
+            0x045e, // Microsoft
+            0x0738, // Mad Catz
+            0x0e6f, // Unknown
+            0x0f0d, // Hori
+            0x1532, // Razer Wildcat
+            0x24c6, // PowerA
+        };
+
+        if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
+            usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
+            usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
+            int vendor_id = usbDevice.getVendorId();
+            for (int supportedVid : SUPPORTED_VENDORS) {
+                if (vendor_id == supportedVid) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private void handleUsbDeviceAttached(UsbDevice usbDevice) {
+        if (isHIDDeviceUSB(usbDevice)) {
+            connectHIDDeviceUSB(usbDevice);
+        }
+    }
+
+    private void handleUsbDeviceDetached(UsbDevice usbDevice) {
+        HIDDeviceUSB device = mUSBDevices.get(usbDevice);
+        if (device == null)
+            return;
+
+        int id = device.getId();
+        mUSBDevices.remove(usbDevice);
+        mDevicesById.remove(id);
+        device.shutdown();
+        HIDDeviceDisconnected(id);
+    }
+
+    private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
+        HIDDeviceUSB device = mUSBDevices.get(usbDevice);
+        if (device == null)
+            return;
+
+        boolean opened = false;
+        if (permission_granted) {
+            opened = device.open();
+        }
+        HIDDeviceOpenResult(device.getId(), opened);
+    }
+
+    private void connectHIDDeviceUSB(UsbDevice usbDevice) {
+        synchronized (this) {
+            for (int interface_number = 0; interface_number < usbDevice.getInterfaceCount(); interface_number++) {
+                if (isHIDDeviceInterface(usbDevice, interface_number)) {
+                    HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_number);
+                    int id = device.getId();
+                    mUSBDevices.put(usbDevice, device);
+                    mDevicesById.put(id, device);
+                    HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), interface_number);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void initializeBluetooth() {
+        Log.d(TAG, "Initializing Bluetooth");
+
+        if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
+            Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
+            return;
+        }
+
+        // Find bonded bluetooth controllers and create SteamControllers for them
+        mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
+        if (mBluetoothManager == null) {
+            // This device doesn't support Bluetooth.
+            return;
+        }
+
+        BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
+        if (btAdapter == null) {
+            // This device has Bluetooth support in the codebase, but has no available adapters.
+            return;
+        }
+
+        // Get our bonded devices.
+        for (BluetoothDevice device : btAdapter.getBondedDevices()) {
+
+            Log.d(TAG, "Bluetooth device available: " + device);
+            if (isSteamController(device)) {
+                connectBluetoothDevice(device);
+            }
+
+        }
+
+        // NOTE: These don't work on Chromebooks, to my undying dismay.
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
+        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
+        mContext.registerReceiver(mBluetoothBroadcast, filter);
+
+        if (mIsChromebook) {
+            mHandler = new Handler(Looper.getMainLooper());
+            mLastBluetoothDevices = new ArrayList<>();
+
+            // final HIDDeviceManager finalThis = this;
+            // mHandler.postDelayed(new Runnable() {
+            //     @Override
+            //     public void run() {
+            //         finalThis.chromebookConnectionHandler();
+            //     }
+            // }, 5000);
+        }
+    }
+
+    private void shutdownBluetooth() {
+        try {
+            mContext.unregisterReceiver(mBluetoothBroadcast);
+        } catch (Exception e) {
+            // We may not have registered, that's okay
+        }
+    }
+
+    // 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() {
+        if (!mIsChromebook) {
+            return;
+        }
+
+        ArrayList<BluetoothDevice> disconnected = new ArrayList<>();
+        ArrayList<BluetoothDevice> connected = new ArrayList<>();
+
+        List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
+
+        for (BluetoothDevice bluetoothDevice : currentConnected) {
+            if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
+                connected.add(bluetoothDevice);
+            }
+        }
+        for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
+            if (!currentConnected.contains(bluetoothDevice)) {
+                disconnected.add(bluetoothDevice);
+            }
+        }
+
+        mLastBluetoothDevices = currentConnected;
+
+        for (BluetoothDevice bluetoothDevice : disconnected) {
+            disconnectBluetoothDevice(bluetoothDevice);
+        }
+        for (BluetoothDevice bluetoothDevice : connected) {
+            connectBluetoothDevice(bluetoothDevice);
+        }
+
+        final HIDDeviceManager finalThis = this;
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                finalThis.chromebookConnectionHandler();
+            }
+        }, 10000);
+    }
+
+    public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
+        Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
+        synchronized (this) {
+            if (mBluetoothDevices.containsKey(bluetoothDevice)) {
+                Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
+
+                HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
+                device.reconnect();
+
+                return false;
+            }
+            HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
+            int id = device.getId();
+            mBluetoothDevices.put(bluetoothDevice, device);
+            mDevicesById.put(id, device);
+
+            // The Steam Controller will mark itself connected once initialization is complete
+        }
+        return true;
+    }
+
+    public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
+        synchronized (this) {
+            HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
+            if (device == null)
+                return;
+
+            int id = device.getId();
+            mBluetoothDevices.remove(bluetoothDevice);
+            mDevicesById.remove(id);
+            device.shutdown();
+            HIDDeviceDisconnected(id);
+        }
+    }
+
+    public 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;
+        }
+
+        // If the device has no local name, we really don't want to try an equality check against it.
+        if (bluetoothDevice.getName() == null) {
+            return false;
+        }
+
+        return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
+    }
+
+    private void close() {
+        shutdownUSB();
+        shutdownBluetooth();
+        synchronized (this) {
+            for (HIDDevice device : mDevicesById.values()) {
+                device.shutdown();
+            }
+            mDevicesById.clear();
+            mBluetoothDevices.clear();
+            HIDDeviceReleaseCallback();
+        }
+    }
+
+    public void setFrozen(boolean frozen) {
+        synchronized (this) {
+            for (HIDDevice device : mDevicesById.values()) {
+                device.setFrozen(frozen);
+            }
+        }        
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private HIDDevice getDevice(int id) {
+        synchronized (this) {
+            HIDDevice result = mDevicesById.get(id);
+            if (result == null) {
+                Log.v(TAG, "No device for id: " + id);
+                Log.v(TAG, "Available devices: " + mDevicesById.keySet());
+            }
+            return result;
+        }
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    ////////// JNI interface functions
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean openDevice(int deviceID) {
+        // Look to see if this is a USB device and we have permission to access it
+        for (HIDDeviceUSB device : mUSBDevices.values()) {
+            if (deviceID == device.getId()) {
+                UsbDevice usbDevice = device.getDevice();
+                if (!mUsbManager.hasPermission(usbDevice)) {
+                    HIDDeviceOpenPending(deviceID);
+                    try {
+                        mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0));
+                    } catch (Exception e) {
+                        Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
+                        HIDDeviceOpenResult(deviceID, false);
+                    }
+                    return false;
+                }
+                break;
+            }
+        }
+
+        try {
+            Log.v(TAG, "openDevice deviceID=" + deviceID);
+            HIDDevice device;
+            device = getDevice(deviceID);
+            if (device == null) {
+                HIDDeviceDisconnected(deviceID);
+                return false;
+            }
+
+            return device.open();
+        } catch (Exception e) {
+            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
+        }
+        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) {
+        try {
+            Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
+            HIDDevice device;
+            device = getDevice(deviceID);
+            if (device == null) {
+                HIDDeviceDisconnected(deviceID);
+                return -1;
+            }
+
+            return device.sendFeatureReport(report);
+        } catch (Exception e) {
+            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
+        }
+        return -1;
+    }
+
+    public boolean getFeatureReport(int deviceID, byte[] report) {
+        try {
+            Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
+            HIDDevice device;
+            device = getDevice(deviceID);
+            if (device == null) {
+                HIDDeviceDisconnected(deviceID);
+                return false;
+            }
+
+            return device.getFeatureReport(report);
+        } catch (Exception e) {
+            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
+        }
+        return false;
+    }
+
+    public void closeDevice(int deviceID) {
+        try {
+            Log.v(TAG, "closeDevice deviceID=" + deviceID);
+            HIDDevice device;
+            device = getDevice(deviceID);
+            if (device == null) {
+                HIDDeviceDisconnected(deviceID);
+                return;
+            }
+
+            device.close();
+        } catch (Exception e) {
+            Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
+        }
+    }
+
+
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+    /////////////// Native methods
+    //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+    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);
+    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);
+}

+ 307 - 0
oxygine/SDL/android/lib/src/org/libsdl/app/HIDDeviceUSB.java

@@ -0,0 +1,307 @@
+package org.libsdl.app;
+
+import android.hardware.usb.*;
+import android.os.Build;
+import android.util.Log;
+import java.util.Arrays;
+
+class HIDDeviceUSB implements HIDDevice {
+
+    private static final String TAG = "hidapi";
+
+    protected HIDDeviceManager mManager;
+    protected UsbDevice mDevice;
+    protected int mInterface;
+    protected int mDeviceId;
+    protected UsbDeviceConnection mConnection;
+    protected UsbEndpoint mInputEndpoint;
+    protected UsbEndpoint mOutputEndpoint;
+    protected InputThread mInputThread;
+    protected boolean mRunning;
+    protected boolean mFrozen;
+
+    public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_number) {
+        mManager = manager;
+        mDevice = usbDevice;
+        mInterface = interface_number;
+        mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
+        mRunning = false;
+    }
+
+    public String getIdentifier() {
+        return String.format("%s/%x/%x", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId());
+    }
+
+    @Override
+    public int getId() {
+        return mDeviceId;
+    }
+
+    @Override
+    public int getVendorId() {
+        return mDevice.getVendorId();
+    }
+
+    @Override
+    public int getProductId() {
+        return mDevice.getProductId();
+    }
+
+    @Override
+    public String getSerialNumber() {
+        String result = null;
+        if (Build.VERSION.SDK_INT >= 21) {
+            result = mDevice.getSerialNumber();
+        }
+        if (result == null) {
+            result = "";
+        }
+        return result;
+    }
+
+    @Override
+    public int getVersion() {
+        return 0;
+    }
+
+    @Override
+    public String getManufacturerName() {
+        String result = null;
+        if (Build.VERSION.SDK_INT >= 21) {
+            result = mDevice.getManufacturerName();
+        }
+        if (result == null) {
+            result = String.format("%x", getVendorId());
+        }
+        return result;
+    }
+
+    @Override
+    public String getProductName() {
+        String result = null;
+        if (Build.VERSION.SDK_INT >= 21) {
+            result = mDevice.getProductName();
+        }
+        if (result == null) {
+            result = String.format("%x", getProductId());
+        }
+        return result;
+    }
+
+    public UsbDevice getDevice() {
+        return mDevice;
+    }
+
+    public String getDeviceName() {
+        return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
+    }
+
+    @Override
+    public boolean open() {
+        mConnection = mManager.getUSBManager().openDevice(mDevice);
+        if (mConnection == null) {
+            Log.w(TAG, "Unable to open USB device " + getDeviceName());
+            return false;
+        }
+
+        // Force claim all interfaces
+        for (int i = 0; i < mDevice.getInterfaceCount(); i++) {
+            UsbInterface iface = mDevice.getInterface(i);
+
+            if (!mConnection.claimInterface(iface, true)) {
+                Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
+                close();
+                return false;
+            }
+        }
+
+        // Find the endpoints
+        UsbInterface iface = mDevice.getInterface(mInterface);
+        for (int j = 0; j < iface.getEndpointCount(); j++) {
+            UsbEndpoint endpt = iface.getEndpoint(j);
+            switch (endpt.getDirection()) {
+            case UsbConstants.USB_DIR_IN:
+                if (mInputEndpoint == null) {
+                    mInputEndpoint = endpt;
+                }
+                break;
+            case UsbConstants.USB_DIR_OUT:
+                if (mOutputEndpoint == null) {
+                    mOutputEndpoint = endpt;
+                }
+                break;
+            }
+        }
+
+        // Make sure the required endpoints were present
+        if (mInputEndpoint == null || mOutputEndpoint == null) {
+            Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
+            close();
+            return false;
+        }
+
+        // Start listening for input
+        mRunning = true;
+        mInputThread = new InputThread();
+        mInputThread.start();
+
+        return true;
+    }
+
+    @Override
+    public int sendFeatureReport(byte[] report) {
+        int res = -1;
+        int offset = 0;
+        int length = report.length;
+        boolean skipped_report_id = false;
+        byte report_number = report[0];
+
+        if (report_number == 0x0) {
+            ++offset;
+            --length;
+            skipped_report_id = true;
+        }
+
+        res = mConnection.controlTransfer(
+            UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
+            0x09/*HID set_report*/,
+            (3/*HID feature*/ << 8) | report_number,
+            0,
+            report, offset, length,
+            1000/*timeout millis*/);
+
+        if (res < 0) {
+            Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
+            return -1;
+        }
+
+        if (skipped_report_id) {
+            ++length;
+        }
+        return length;
+    }
+
+    @Override
+    public int sendOutputReport(byte[] report) {
+        int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
+        if (r != report.length) {
+            Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
+        }
+        return r;
+    }
+
+    @Override
+    public boolean getFeatureReport(byte[] report) {
+        int res = -1;
+        int offset = 0;
+        int length = report.length;
+        boolean skipped_report_id = false;
+        byte report_number = report[0];
+
+        if (report_number == 0x0) {
+            /* Offset the return buffer by 1, so that the report ID
+               will remain in byte 0. */
+            ++offset;
+            --length;
+            skipped_report_id = true;
+        }
+
+        res = mConnection.controlTransfer(
+            UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
+            0x01/*HID get_report*/,
+            (3/*HID feature*/ << 8) | report_number,
+            0,
+            report, offset, length,
+            1000/*timeout millis*/);
+
+        if (res < 0) {
+            Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
+            return false;
+        }
+
+        if (skipped_report_id) {
+            ++res;
+            ++length;
+        }
+
+        byte[] data;
+        if (res == length) {
+            data = report;
+        } else {
+            data = Arrays.copyOfRange(report, 0, res);
+        }
+        mManager.HIDDeviceFeatureReport(mDeviceId, data);
+
+        return true;
+    }
+
+    @Override
+    public void close() {
+        mRunning = false;
+        if (mInputThread != null) {
+            while (mInputThread.isAlive()) {
+                mInputThread.interrupt();
+                try {
+                    mInputThread.join();
+                } catch (InterruptedException e) {
+                    // Keep trying until we're done
+                }
+            }
+            mInputThread = null;
+        }
+        if (mConnection != null) {
+            for (int i = 0; i < mDevice.getInterfaceCount(); i++) {
+                UsbInterface iface = mDevice.getInterface(i);
+                mConnection.releaseInterface(iface);
+            }
+            mConnection.close();
+            mConnection = null;
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        close();
+        mManager = null;
+    }
+
+    @Override
+    public void setFrozen(boolean frozen) {
+        mFrozen = frozen;
+    }
+
+    protected class InputThread extends Thread {
+        @Override
+        public void run() {
+            int packetSize = mInputEndpoint.getMaxPacketSize();
+            byte[] packet = new byte[packetSize];
+            while (mRunning) {
+                int r;
+                try
+                {
+                    r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
+                }
+                catch (Exception e)
+                {
+                    Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
+                    break;
+                }
+                if (r < 0) {
+                    // Could be a timeout or an I/O error
+                }
+                if (r > 0) {
+                    byte[] data;
+                    if (r == packetSize) {
+                        data = packet;
+                    } else {
+                        data = Arrays.copyOfRange(packet, 0, r);
+                    }
+
+                    if (!mFrozen) {
+                        mManager.HIDDeviceInputReport(mDeviceId, data);
+                    }
+                }
+            }
+        }
+    }
+}

+ 47 - 0
oxygine/SDL/android/lib/src/org/libsdl/app/SDL.java

@@ -2,6 +2,8 @@ package org.libsdl.app;
 
 
 import android.content.Context;
 import android.content.Context;
 
 
+import java.lang.reflect.*;
+
 /**
 /**
     SDL library initialization
     SDL library initialization
 */
 */
@@ -33,5 +35,50 @@ public class SDL {
         return mContext;
         return mContext;
     }
     }
 
 
+    public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
+
+        if (libraryName == null) {
+            throw new NullPointerException("No library name provided.");
+        }
+
+        try {
+            // Let's see if we have ReLinker available in the project.  This is necessary for 
+            // some projects that have huge numbers of local libraries bundled, and thus may 
+            // trip a bug in Android's native library loader which ReLinker works around.  (If
+            // loadLibrary works properly, ReLinker will simply use the normal Android method
+            // internally.)
+            //
+            // To use ReLinker, just add it as a dependency.  For more information, see 
+            // https://github.com/KeepSafe/ReLinker for ReLinker's repository.
+            //
+            Class relinkClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
+            Class relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
+            Class contextClass = mContext.getClassLoader().loadClass("android.content.Context");
+            Class stringClass = mContext.getClassLoader().loadClass("java.lang.String");
+
+            // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if 
+            // they've changed during updates.
+            Method forceMethod = relinkClass.getDeclaredMethod("force");
+            Object relinkInstance = forceMethod.invoke(null);
+            Class relinkInstanceClass = relinkInstance.getClass();
+
+            // Actually load the library!
+            Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
+            loadMethod.invoke(relinkInstance, mContext, libraryName, null, null);
+        }
+        catch (final Exception e) {
+            // Fall back
+            try {
+                System.loadLibrary(libraryName);
+            }
+            catch (final UnsatisfiedLinkError ule) {
+                throw ule;
+            }
+            catch (final SecurityException se) {
+                throw se;
+            }
+        }        
+    }
+
     protected static Context mContext;
     protected static Context mContext;
 }
 }

+ 516 - 33
oxygine/SDL/android/lib/src/org/libsdl/app/SDLActivity.java

@@ -3,10 +3,13 @@ package org.libsdl.app;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.Arrays;
+import java.util.Hashtable;
 import java.lang.reflect.Method;
 import java.lang.reflect.Method;
+import java.lang.Math;
 
 
 import android.app.*;
 import android.app.*;
 import android.content.*;
 import android.content.*;
+import android.content.res.Configuration;
 import android.text.InputType;
 import android.text.InputType;
 import android.view.*;
 import android.view.*;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.BaseInputConnection;
@@ -31,11 +34,34 @@ import android.content.pm.ApplicationInfo;
 /**
 /**
     SDL Activity
     SDL Activity
 */
 */
-public class SDLActivity extends Activity {
+public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener {
     private static final String TAG = "SDL";
     private static final String TAG = "SDL";
 
 
     public static boolean mIsResumedCalled, mIsSurfaceReady, mHasFocus;
     public static boolean mIsResumedCalled, mIsSurfaceReady, mHasFocus;
 
 
+    // Cursor types
+    private static final int SDL_SYSTEM_CURSOR_NONE = -1;
+    private static final int SDL_SYSTEM_CURSOR_ARROW = 0;
+    private static final int SDL_SYSTEM_CURSOR_IBEAM = 1;
+    private static final int SDL_SYSTEM_CURSOR_WAIT = 2;
+    private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3;
+    private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4;
+    private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5;
+    private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6;
+    private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7;
+    private static final int SDL_SYSTEM_CURSOR_SIZENS = 8;
+    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;
+
+    protected static final int SDL_ORIENTATION_UNKNOWN = 0;
+    protected static final int SDL_ORIENTATION_LANDSCAPE = 1;
+    protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2;
+    protected static final int SDL_ORIENTATION_PORTRAIT = 3;
+    protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4;
+
+    protected static int mCurrentOrientation;
+
     // Handle the state of the native layer
     // Handle the state of the native layer
     public enum NativeState {
     public enum NativeState {
            INIT, RESUMED, PAUSED
            INIT, RESUMED, PAUSED
@@ -60,11 +86,29 @@ public class SDLActivity extends Activity {
     protected static boolean mScreenKeyboardShown;
     protected static boolean mScreenKeyboardShown;
     protected static ViewGroup mLayout;
     protected static ViewGroup mLayout;
     protected static SDLClipboardHandler mClipboardHandler;
     protected static SDLClipboardHandler mClipboardHandler;
-
+    protected static Hashtable<Integer, Object> mCursors;
+    protected static int mLastCursorID;
+    protected static SDLGenericMotionListener_API12 mMotionListener;
+    protected static HIDDeviceManager mHIDDeviceManager;
 
 
     // This is what SDL runs in. It invokes SDL_main(), eventually
     // This is what SDL runs in. It invokes SDL_main(), eventually
     protected static Thread mSDLThread;
     protected static Thread mSDLThread;
 
 
+    protected static SDLGenericMotionListener_API12 getMotionListener() {
+        if (mMotionListener == null) {
+            if (Build.VERSION.SDK_INT >= 26) {
+                mMotionListener = new SDLGenericMotionListener_API26();
+            } else 
+            if (Build.VERSION.SDK_INT >= 24) {
+                mMotionListener = new SDLGenericMotionListener_API24();
+            } else {
+                mMotionListener = new SDLGenericMotionListener_API12();
+            }
+        }
+
+        return mMotionListener;
+    }
+
     /**
     /**
      * This method returns the name of the shared object with the application entry point
      * This method returns the name of the shared object with the application entry point
      * It can be overridden by derived classes.
      * It can be overridden by derived classes.
@@ -77,7 +121,7 @@ public class SDLActivity extends Activity {
         } else {
         } else {
             library = "libmain.so";
             library = "libmain.so";
         }
         }
-        return library;
+        return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
     }
     }
 
 
     /**
     /**
@@ -110,7 +154,7 @@ public class SDLActivity extends Activity {
     // Load the .so
     // Load the .so
     public void loadLibraries() {
     public void loadLibraries() {
        for (String lib : getLibraries()) {
        for (String lib : getLibraries()) {
-          System.loadLibrary(lib);
+          SDL.loadLibrary(lib);
        }
        }
     }
     }
 
 
@@ -132,6 +176,8 @@ public class SDLActivity extends Activity {
         mTextEdit = null;
         mTextEdit = null;
         mLayout = null;
         mLayout = null;
         mClipboardHandler = null;
         mClipboardHandler = null;
+        mCursors = new Hashtable<Integer, Object>();
+        mLastCursorID = 0;
         mSDLThread = null;
         mSDLThread = null;
         mExitCalledFromJava = false;
         mExitCalledFromJava = false;
         mBrokenLibraries = false;
         mBrokenLibraries = false;
@@ -145,8 +191,8 @@ public class SDLActivity extends Activity {
     // Setup
     // Setup
     @Override
     @Override
     protected void onCreate(Bundle savedInstanceState) {
     protected void onCreate(Bundle savedInstanceState) {
-        Log.v(TAG, "Device: " + android.os.Build.DEVICE);
-        Log.v(TAG, "Model: " + android.os.Build.MODEL);
+        Log.v(TAG, "Device: " + Build.DEVICE);
+        Log.v(TAG, "Model: " + Build.MODEL);
         Log.v(TAG, "onCreate()");
         Log.v(TAG, "onCreate()");
         super.onCreate(savedInstanceState);
         super.onCreate(savedInstanceState);
 
 
@@ -204,27 +250,23 @@ public class SDLActivity extends Activity {
             mClipboardHandler = new SDLClipboardHandler_Old();
             mClipboardHandler = new SDLClipboardHandler_Old();
         }
         }
 
 
+        mHIDDeviceManager = HIDDeviceManager.acquire(this);
+
         // Set up the surface
         // Set up the surface
         mSurface = new SDLSurface(getApplication());
         mSurface = new SDLSurface(getApplication());
 
 
         mLayout = new RelativeLayout(this);
         mLayout = new RelativeLayout(this);
         mLayout.addView(mSurface);
         mLayout.addView(mSurface);
 
 
+        // Get our current screen orientation and pass it down.
+        mCurrentOrientation = SDLActivity.getCurrentOrientation();
+        SDLActivity.onNativeOrientationChanged(mCurrentOrientation);
+
         setContentView(mLayout);
         setContentView(mLayout);
 
 
-        /* 
-         * Per SDL_androidwindow.c, Android will only ever have one window, and that window 
-         * is always flagged SDL_WINDOW_FULLSCREEN.  Let's treat it as an immersive fullscreen 
-         * window for Android UI purposes, as a result.
-         */
-        int iFlags = 
-            //View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | // Only available since API 19
-            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
-            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
-            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
-            View.SYSTEM_UI_FLAG_FULLSCREEN;
+        setWindowStyle(true);
 
 
-        getWindow().getDecorView().setSystemUiVisibility(iFlags);        
+        getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
 
 
         // Get filename from "Open with" of another application
         // Get filename from "Open with" of another application
         Intent intent = getIntent();
         Intent intent = getIntent();
@@ -249,6 +291,10 @@ public class SDLActivity extends Activity {
            return;
            return;
         }
         }
 
 
+        if (mHIDDeviceManager != null) {
+            mHIDDeviceManager.setFrozen(true);
+        }
+
         SDLActivity.handleNativeState();
         SDLActivity.handleNativeState();
     }
     }
 
 
@@ -263,9 +309,39 @@ public class SDLActivity extends Activity {
            return;
            return;
         }
         }
 
 
+        if (mHIDDeviceManager != null) {
+            mHIDDeviceManager.setFrozen(false);
+        }
+
         SDLActivity.handleNativeState();
         SDLActivity.handleNativeState();
     }
     }
 
 
+    public static int getCurrentOrientation() {
+        final Context context = SDLActivity.getContext();
+        final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+
+        int result = SDL_ORIENTATION_UNKNOWN;
+
+        switch (display.getRotation()) {
+            case Surface.ROTATION_0:
+                result = SDL_ORIENTATION_PORTRAIT;
+                break;
+    
+            case Surface.ROTATION_90:
+                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;
+        }
+
+        return result;
+    }
 
 
     @Override
     @Override
     public void onWindowFocusChanged(boolean hasFocus) {
     public void onWindowFocusChanged(boolean hasFocus) {
@@ -279,6 +355,7 @@ public class SDLActivity extends Activity {
         SDLActivity.mHasFocus = hasFocus;
         SDLActivity.mHasFocus = hasFocus;
         if (hasFocus) {
         if (hasFocus) {
            mNextNativeState = NativeState.RESUMED;
            mNextNativeState = NativeState.RESUMED;
+           SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded();
         } else {
         } else {
            mNextNativeState = NativeState.PAUSED;
            mNextNativeState = NativeState.PAUSED;
         }
         }
@@ -302,6 +379,11 @@ public class SDLActivity extends Activity {
     protected void onDestroy() {
     protected void onDestroy() {
         Log.v(TAG, "onDestroy()");
         Log.v(TAG, "onDestroy()");
 
 
+        if (mHIDDeviceManager != null) {
+            HIDDeviceManager.release(mHIDDeviceManager);
+            mHIDDeviceManager = null;
+        }
+
         if (SDLActivity.mBrokenLibraries) {
         if (SDLActivity.mBrokenLibraries) {
            super.onDestroy();
            super.onDestroy();
            // Reset everything in case the user re opens the app
            // Reset everything in case the user re opens the app
@@ -334,6 +416,43 @@ public class SDLActivity extends Activity {
         SDLActivity.initialize();
         SDLActivity.initialize();
     }
     }
 
 
+    @Override
+    public void onBackPressed() {
+        // Check if we want to block the back button in case of mouse right click.
+        //
+        // If we do, the normal hardware back button will no longer work and people have to use home,
+        // but the mouse right click will work.
+        //
+        String trapBack = SDLActivity.nativeGetHint("SDL_ANDROID_TRAP_BACK_BUTTON");
+        if ((trapBack != null) && trapBack.equals("1")) {
+            // Exit and let the mouse handler handle this button (if appropriate)
+            return;
+        }
+
+        // Default system back button behavior.
+        super.onBackPressed();
+    }
+
+    // Called by JNI from SDL.
+    public static void manualBackButton() {
+        mSingleton.pressBackButton();
+    }
+
+    // Used to get us onto the activity's main thread
+    public void pressBackButton() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                SDLActivity.this.superOnBackPressed();
+            }
+        });
+    }
+
+    // Used to access the system back behavior.
+    public void superOnBackPressed() {
+        super.onBackPressed();
+    }
+
     @Override
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
     public boolean dispatchKeyEvent(KeyEvent event) {
 
 
@@ -401,18 +520,22 @@ public class SDLActivity extends Activity {
     /* The native thread has finished */
     /* The native thread has finished */
     public static void handleNativeExit() {
     public static void handleNativeExit() {
         SDLActivity.mSDLThread = null;
         SDLActivity.mSDLThread = null;
-        mSingleton.finish();
+        if (mSingleton != null) {
+            mSingleton.finish();
+        }
     }
     }
 
 
 
 
     // Messages from the SDLMain thread
     // Messages from the SDLMain thread
     static final int COMMAND_CHANGE_TITLE = 1;
     static final int COMMAND_CHANGE_TITLE = 1;
-    static final int COMMAND_UNUSED = 2;
+    static final int COMMAND_CHANGE_WINDOW_STYLE = 2;
     static final int COMMAND_TEXTEDIT_HIDE = 3;
     static final int COMMAND_TEXTEDIT_HIDE = 3;
     static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
     static final int COMMAND_SET_KEEP_SCREEN_ON = 5;
 
 
     protected static final int COMMAND_USER = 0x8000;
     protected static final int COMMAND_USER = 0x8000;
 
 
+    protected static boolean mFullscreenModeActive;
+
     /**
     /**
      * This method is called by SDL if SDL did not handle a message itself.
      * This method is called by SDL if SDL did not handle a message itself.
      * This happens if a received message contains an unsupported command.
      * This happens if a received message contains an unsupported command.
@@ -446,6 +569,37 @@ public class SDLActivity extends Activity {
                     Log.e(TAG, "error handling message, getContext() returned no Activity");
                     Log.e(TAG, "error handling message, getContext() returned no Activity");
                 }
                 }
                 break;
                 break;
+            case COMMAND_CHANGE_WINDOW_STYLE:
+                if (Build.VERSION.SDK_INT < 19) {
+                    // This version of Android doesn't support the immersive fullscreen mode
+                    break;
+                }
+                if (context instanceof Activity) {
+                    Window window = ((Activity) context).getWindow();
+                    if (window != null) {
+                        if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 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;
+                        }
+                    }
+                } else {
+                    Log.e(TAG, "error handling message, getContext() returned no Activity");
+                }
+                break;
             case COMMAND_TEXTEDIT_HIDE:
             case COMMAND_TEXTEDIT_HIDE:
                 if (mTextEdit != null) {
                 if (mTextEdit != null) {
                     // Note: On some devices setting view to GONE creates a flicker in landscape.
                     // Note: On some devices setting view to GONE creates a flicker in landscape.
@@ -489,7 +643,22 @@ public class SDLActivity extends Activity {
         Message msg = commandHandler.obtainMessage();
         Message msg = commandHandler.obtainMessage();
         msg.arg1 = command;
         msg.arg1 = command;
         msg.obj = data;
         msg.obj = data;
-        return commandHandler.sendMessage(msg);
+        boolean result = commandHandler.sendMessage(msg);
+
+        // Ensure we don't return until the resize has actually happened,
+        // or 250ms have passed.
+        if (command == COMMAND_CHANGE_WINDOW_STYLE) {
+            synchronized(SDLActivity.getContext()) {
+                try {
+                    SDLActivity.getContext().wait(250);
+                }
+                catch (InterruptedException ie) {
+                    ie.printStackTrace();
+                }
+            }
+        }
+
+        return result;
     }
     }
 
 
     // C functions we call
     // C functions we call
@@ -500,11 +669,11 @@ public class SDLActivity extends Activity {
     public static native void nativePause();
     public static native void nativePause();
     public static native void nativeResume();
     public static native void nativeResume();
     public static native void onNativeDropFile(String filename);
     public static native void onNativeDropFile(String filename);
-    public static native void onNativeResize(int x, int y, int format, float rate);
+    public static native void onNativeResize(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate);
     public static native void onNativeKeyDown(int keycode);
     public static native void onNativeKeyDown(int keycode);
     public static native void onNativeKeyUp(int keycode);
     public static native void onNativeKeyUp(int keycode);
     public static native void onNativeKeyboardFocusLost();
     public static native void onNativeKeyboardFocusLost();
-    public static native void onNativeMouse(int button, int action, float x, float y);
+    public static native void onNativeMouse(int button, int action, float x, float y, boolean relative);
     public static native void onNativeTouch(int touchDevId, int pointerFingerId,
     public static native void onNativeTouch(int touchDevId, int pointerFingerId,
                                             int action, float x,
                                             int action, float x,
                                             float y, float p);
                                             float y, float p);
@@ -514,6 +683,7 @@ public class SDLActivity extends Activity {
     public static native void onNativeSurfaceDestroyed();
     public static native void onNativeSurfaceDestroyed();
     public static native String nativeGetHint(String name);
     public static native String nativeGetHint(String name);
     public static native void nativeSetenv(String name, String value);
     public static native void nativeSetenv(String name, String value);
+    public static native void onNativeOrientationChanged(int orientation);
 
 
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
@@ -523,6 +693,14 @@ public class SDLActivity extends Activity {
         return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
         return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title);
     }
     }
 
 
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static void setWindowStyle(boolean fullscreen) {
+        // Called from SDLMain() thread and can't directly affect the view
+        mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0);
+    }
+
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      * This is a static method for JNI convenience, it calls a non-static method
      * This is a static method for JNI convenience, it calls a non-static method
@@ -575,7 +753,6 @@ public class SDLActivity extends Activity {
         }
         }
     }
     }
 
 
-
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      */
      */
@@ -594,6 +771,42 @@ public class SDLActivity extends Activity {
 
 
     }
     }
 
 
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean supportsRelativeMouse()
+    {
+        // ChromeOS doesn't provide relative mouse motion via the Android 7 APIs
+        if (isChromebook()) {
+            return false;
+        }
+
+        // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under 
+        // Android 7 APIs, and simply returns no data under Android 8 APIs.
+        //
+        // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and
+        // thus SDK version 27.  If we are in DeX mode and not API 27 or higher, as a result,
+        // we should stick to relative mode.
+        //
+        if ((Build.VERSION.SDK_INT < 27) && isDeXMode()) {
+            return false;
+        }
+
+        return SDLActivity.getMotionListener().supportsRelativeMouse();
+    }
+
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean setRelativeMouseEnabled(boolean enabled)
+    {
+        if (enabled && !supportsRelativeMouse()) {
+            return false;
+        }
+
+        return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled);
+    }
+
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      */
      */
@@ -611,6 +824,67 @@ public class SDLActivity extends Activity {
         return SDL.getContext();
         return SDL.getContext();
     }
     }
 
 
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean isAndroidTV() {
+        UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE);
+        if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
+            return true;
+        }
+        if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) {
+            return true;
+        }
+        if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean isTablet() {
+        DisplayMetrics metrics = new DisplayMetrics();
+        Activity activity = (Activity)getContext();
+        activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
+
+        double dWidthInches = metrics.widthPixels / (double)metrics.xdpi;
+        double dHeightInches = metrics.heightPixels / (double)metrics.ydpi;
+
+        double dDiagonal = Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches));
+
+        // If our diagonal size is seven inches or greater, we consider ourselves a tablet.
+        return (dDiagonal >= 7.0);
+    }
+
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean isChromebook() {
+        return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
+    }    
+
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean isDeXMode() {
+        if (Build.VERSION.SDK_INT < 24) {
+            return false;
+        }
+        try {
+            final Configuration config = getContext().getResources().getConfiguration();
+            final Class configClass = config.getClass();
+            return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass)
+                    == configClass.getField("semDesktopModeEnabled").getInt(config);
+        } catch(Exception ignored) {
+            return false;
+        }
+    }
+
+    /**
+     * This method is called by SDL using JNI.
+     */
     public static DisplayMetrics getDisplayDPI() {
     public static DisplayMetrics getDisplayDPI() {
         return getContext().getResources().getDisplayMetrics();
         return getContext().getResources().getDisplayMetrics();
     }
     }
@@ -625,7 +899,7 @@ public class SDLActivity extends Activity {
             if (bundle == null) {
             if (bundle == null) {
                 return false;
                 return false;
             }
             }
-			String prefix = "SDL_ENV.";
+            String prefix = "SDL_ENV.";
             final int trimLength = prefix.length();
             final int trimLength = prefix.length();
             for (String key : bundle.keySet()) {
             for (String key : bundle.keySet()) {
                 if (key.startsWith(prefix)) {
                 if (key.startsWith(prefix)) {
@@ -642,6 +916,12 @@ public class SDLActivity extends Activity {
         return false;
         return false;
     }
     }
 
 
+    // This method is called by SDLControllerManager's API 26 Generic Motion Handler.
+    public static View getContentView() 
+    {
+        return mSingleton.mLayout;
+    }
+
     static class ShowTextInputTask implements Runnable {
     static class ShowTextInputTask implements Runnable {
         /*
         /*
          * This is used to regulate the pan&scan method to have some offset from
          * This is used to regulate the pan&scan method to have some offset from
@@ -694,7 +974,7 @@ public class SDLActivity extends Activity {
     public static boolean isTextInputEvent(KeyEvent event) {
     public static boolean isTextInputEvent(KeyEvent event) {
       
       
         // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT
         // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT
-        if (android.os.Build.VERSION.SDK_INT >= 11) {
+        if (Build.VERSION.SDK_INT >= 11) {
             if (event.isCtrlPressed()) {
             if (event.isCtrlPressed()) {
                 return false;
                 return false;
             }  
             }  
@@ -1007,13 +1287,39 @@ public class SDLActivity extends Activity {
         return dialog;
         return dialog;
     }
     }
 
 
+    private final Runnable rehideSystemUi = new Runnable() {
+        @Override
+        public void run() {
+            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);
+        }
+    };
+
+    public void onSystemUiVisibilityChange(int visibility) {
+        if (SDLActivity.mFullscreenModeActive && (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0 || (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+
+            Handler handler = getWindow().getDecorView().getHandler();
+            if (handler != null) {
+                handler.removeCallbacks(rehideSystemUi); // Prevent a hide loop.
+                handler.postDelayed(rehideSystemUi, 2000);
+            }
+
+        }
+    }    
+
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      */
      */
     public static boolean clipboardHasText() {
     public static boolean clipboardHasText() {
         return mClipboardHandler.clipboardHasText();
         return mClipboardHandler.clipboardHasText();
     }
     }
-    
+
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      */
      */
@@ -1028,6 +1334,94 @@ public class SDLActivity extends Activity {
         mClipboardHandler.clipboardSetText(string);
         mClipboardHandler.clipboardSetText(string);
     }
     }
 
 
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static int createCustomCursor(int[] colors, int width, int height, int hotSpotX, int hotSpotY) {
+        Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
+        ++mLastCursorID;
+        // This requires API 24, so use reflection to implement this
+        try {
+            Class PointerIconClass = Class.forName("android.view.PointerIcon");
+            Class[] arg_types = new Class[] { Bitmap.class, float.class, float.class };
+            Method create = PointerIconClass.getMethod("create", arg_types);
+            mCursors.put(mLastCursorID, create.invoke(null, bitmap, hotSpotX, hotSpotY));
+        } catch (Exception e) {
+            return 0;
+        }
+        return mLastCursorID;
+    }
+
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean setCustomCursor(int cursorID) {
+        // This requires API 24, so use reflection to implement this
+        try {
+            Class PointerIconClass = Class.forName("android.view.PointerIcon");
+            Method setPointerIcon = SDLSurface.class.getMethod("setPointerIcon", PointerIconClass);
+            setPointerIcon.invoke(mSurface, mCursors.get(cursorID));
+        } catch (Exception e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static boolean setSystemCursor(int cursorID) {
+        int cursor_type = 0; //PointerIcon.TYPE_NULL;
+        switch (cursorID) {
+        case SDL_SYSTEM_CURSOR_ARROW:
+            cursor_type = 1000; //PointerIcon.TYPE_ARROW;
+            break;
+        case SDL_SYSTEM_CURSOR_IBEAM:
+            cursor_type = 1008; //PointerIcon.TYPE_TEXT;
+            break;
+        case SDL_SYSTEM_CURSOR_WAIT:
+            cursor_type = 1004; //PointerIcon.TYPE_WAIT;
+            break;
+        case SDL_SYSTEM_CURSOR_CROSSHAIR:
+            cursor_type = 1007; //PointerIcon.TYPE_CROSSHAIR;
+            break;
+        case SDL_SYSTEM_CURSOR_WAITARROW:
+            cursor_type = 1004; //PointerIcon.TYPE_WAIT;
+            break;
+        case SDL_SYSTEM_CURSOR_SIZENWSE:
+            cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
+            break;
+        case SDL_SYSTEM_CURSOR_SIZENESW:
+            cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
+            break;
+        case SDL_SYSTEM_CURSOR_SIZEWE:
+            cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
+            break;
+        case SDL_SYSTEM_CURSOR_SIZENS:
+            cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
+            break;
+        case SDL_SYSTEM_CURSOR_SIZEALL:
+            cursor_type = 1020; //PointerIcon.TYPE_GRAB;
+            break;
+        case SDL_SYSTEM_CURSOR_NO:
+            cursor_type = 1012; //PointerIcon.TYPE_NO_DROP;
+            break;
+        case SDL_SYSTEM_CURSOR_HAND:
+            cursor_type = 1002; //PointerIcon.TYPE_HAND;
+            break;
+        }
+        // This requires API 24, so use reflection to implement this
+        try {
+            Class PointerIconClass = Class.forName("android.view.PointerIcon");
+            Class[] arg_types = new Class[] { Context.class, int.class };
+            Method getSystemIcon = PointerIconClass.getMethod("getSystemIcon", arg_types);
+            Method setPointerIcon = SDLSurface.class.getMethod("setPointerIcon", PointerIconClass);
+            setPointerIcon.invoke(mSurface, getSystemIcon.invoke(null, SDL.getContext(), cursor_type));
+        } catch (Exception e) {
+            return false;
+        }
+        return true;
+    }
 }
 }
 
 
 /**
 /**
@@ -1085,7 +1479,7 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
         mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
         mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
 
 
         if (Build.VERSION.SDK_INT >= 12) {
         if (Build.VERSION.SDK_INT >= 12) {
-            setOnGenericMotionListener(new SDLGenericMotionListener_API12());
+            setOnGenericMotionListener(SDLActivity.getMotionListener());
         }
         }
 
 
         // Some arbitrary defaults to avoid a potential division by zero
         // Some arbitrary defaults to avoid a potential division by zero
@@ -1136,6 +1530,10 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
                                int format, int width, int height) {
                                int format, int width, int height) {
         Log.v("SDL", "surfaceChanged()");
         Log.v("SDL", "surfaceChanged()");
 
 
+        if (SDLActivity.mSingleton == null) {
+            return;
+        }
+
         int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
         int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default
         switch (format) {
         switch (format) {
         case PixelFormat.A_8:
         case PixelFormat.A_8:
@@ -1183,9 +1581,26 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
 
 
         mWidth = width;
         mWidth = width;
         mHeight = height;
         mHeight = height;
-        SDLActivity.onNativeResize(width, height, sdlFormat, mDisplay.getRefreshRate());
-        Log.v("SDL", "Window size: " + width + "x" + height);
+        int nDeviceWidth = width;
+        int nDeviceHeight = height;
+        try
+        {
+            if (Build.VERSION.SDK_INT >= 17) {
+                android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics();
+                mDisplay.getRealMetrics( realMetrics );
+                nDeviceWidth = realMetrics.widthPixels;
+                nDeviceHeight = realMetrics.heightPixels;
+            }
+        }
+        catch ( java.lang.Throwable throwable ) {}
 
 
+        synchronized(SDLActivity.getContext()) {
+            SDLActivity.getContext().notifyAll();
+        }
+
+        Log.v("SDL", "Window size: " + width + "x" + height);
+        Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
+        SDLActivity.onNativeResize(width, height, nDeviceWidth, nDeviceHeight, sdlFormat, mDisplay.getRefreshRate());
 
 
         boolean skip = false;
         boolean skip = false;
         int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
         int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
@@ -1300,7 +1715,8 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
         float x,y,p;
         float x,y,p;
 
 
         // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14.
         // !!! FIXME: dump this SDK check after 2.0.4 ships and require API14.
-        if (event.getSource() == InputDevice.SOURCE_MOUSE && SDLActivity.mSeparateMouseAndTouch) {
+        // 12290 = Samsung DeX mode desktop mouse
+        if ((event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == 12290) && SDLActivity.mSeparateMouseAndTouch) {
             if (Build.VERSION.SDK_INT < 14) {
             if (Build.VERSION.SDK_INT < 14) {
                 mouseButton = 1; // all mouse buttons are the left button
                 mouseButton = 1; // all mouse buttons are the left button
             } else {
             } else {
@@ -1310,7 +1726,14 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
                     mouseButton = 1;    // oh well.
                     mouseButton = 1;    // oh well.
                 }
                 }
             }
             }
-            SDLActivity.onNativeMouse(mouseButton, action, event.getX(0), event.getY(0));
+
+            // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
+            // if we are.  We'll leverage our existing mouse motion listener
+            SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
+            x = motionListener.getEventX(event);
+            y = motionListener.getEventY(event);
+
+            SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
         } else {
         } else {
             switch(action) {
             switch(action) {
                 case MotionEvent.ACTION_MOVE:
                 case MotionEvent.ACTION_MOVE:
@@ -1395,30 +1818,90 @@ class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
     @Override
     @Override
     public void onSensorChanged(SensorEvent event) {
     public void onSensorChanged(SensorEvent event) {
         if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
         if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
+
+            // Since we may have an orientation set, we won't receive onConfigurationChanged events.
+            // We thus should check here.
+            int newOrientation = SDLActivity.SDL_ORIENTATION_UNKNOWN;
+    
             float x, y;
             float x, y;
             switch (mDisplay.getRotation()) {
             switch (mDisplay.getRotation()) {
                 case Surface.ROTATION_90:
                 case Surface.ROTATION_90:
                     x = -event.values[1];
                     x = -event.values[1];
                     y = event.values[0];
                     y = event.values[0];
+                    newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
                     break;
                     break;
                 case Surface.ROTATION_270:
                 case Surface.ROTATION_270:
                     x = event.values[1];
                     x = event.values[1];
                     y = -event.values[0];
                     y = -event.values[0];
+                    newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
                     break;
                     break;
                 case Surface.ROTATION_180:
                 case Surface.ROTATION_180:
                     x = -event.values[1];
                     x = -event.values[1];
                     y = -event.values[0];
                     y = -event.values[0];
+                    newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
                     break;
                     break;
                 default:
                 default:
                     x = event.values[0];
                     x = event.values[0];
                     y = event.values[1];
                     y = event.values[1];
+                    newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
                     break;
                     break;
             }
             }
+
+            if (newOrientation != SDLActivity.mCurrentOrientation) {
+                SDLActivity.mCurrentOrientation = newOrientation;
+                SDLActivity.onNativeOrientationChanged(newOrientation);
+            }
+
             SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
             SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
                                       y / SensorManager.GRAVITY_EARTH,
                                       y / SensorManager.GRAVITY_EARTH,
                                       event.values[2] / SensorManager.GRAVITY_EARTH);
                                       event.values[2] / SensorManager.GRAVITY_EARTH);
+
+            
+        }
+    }
+
+    // Captured pointer events for API 26.
+    public boolean onCapturedPointerEvent(MotionEvent event)
+    {
+        int action = event.getActionMasked();
+
+        float x, y;
+        switch (action) {
+            case MotionEvent.ACTION_SCROLL:
+                x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
+                y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
+                SDLActivity.onNativeMouse(0, action, x, y, false);
+                return true;
+
+            case MotionEvent.ACTION_HOVER_MOVE:
+            case MotionEvent.ACTION_MOVE:
+                x = event.getX(0);
+                y = event.getY(0);
+                SDLActivity.onNativeMouse(0, action, x, y, true);
+                return true;
+
+            case MotionEvent.ACTION_BUTTON_PRESS:
+            case MotionEvent.ACTION_BUTTON_RELEASE:
+
+                // Change our action value to what SDL's code expects.
+                if (action == MotionEvent.ACTION_BUTTON_PRESS) {
+                    action = MotionEvent.ACTION_DOWN;
+                }
+                else if (action == MotionEvent.ACTION_BUTTON_RELEASE) {
+                    action = MotionEvent.ACTION_UP;
+                }
+
+                x = event.getX(0);
+                y = event.getY(0);
+                int button = event.getButtonState();
+
+                SDLActivity.onNativeMouse(button, action, x, y, true);
+                return true;
         }
         }
+
+        return false;
     }
     }
+
 }
 }
 
 
 /* This is a fake invisible editor view that receives the input and defines the
 /* This is a fake invisible editor view that receives the input and defines the
@@ -1508,7 +1991,7 @@ class SDLInputConnection extends BaseInputConnection {
          * as we do with physical keyboards, let's just use it to hide the keyboard.
          * as we do with physical keyboards, let's just use it to hide the keyboard.
          */
          */
 
 
-        if (event.getKeyCode() == 66) {
+        if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
             String imeHide = SDLActivity.nativeGetHint("SDL_RETURN_KEY_HIDES_IME");
             String imeHide = SDLActivity.nativeGetHint("SDL_RETURN_KEY_HIDES_IME");
             if ((imeHide != null) && imeHide.equals("1")) {
             if ((imeHide != null) && imeHide.equals("1")) {
                 Context c = SDL.getContext();
                 Context c = SDL.getContext();

+ 249 - 59
oxygine/SDL/android/lib/src/org/libsdl/app/SDLAudioManager.java

@@ -1,6 +1,7 @@
 package org.libsdl.app;
 package org.libsdl.app;
 
 
 import android.media.*;
 import android.media.*;
+import android.os.Build;
 import android.util.Log;
 import android.util.Log;
 
 
 public class SDLAudioManager
 public class SDLAudioManager
@@ -17,41 +18,250 @@ public class SDLAudioManager
 
 
     // Audio
     // Audio
 
 
-    /**
-     * This method is called by SDL using JNI.
-     */
-    public static int audioOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
-        int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
-        int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
-        int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
+    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 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) {
+            if (desiredChannels > 2) {
+                desiredChannels = 2;
+            }
+            if (sampleRate < 8000) {
+                sampleRate = 8000;
+            } else if (sampleRate > 48000) {
+                sampleRate = 48000;
+            }
+        }
 
 
-        Log.v(TAG, "SDL audio: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
+        if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
+            int minSDKVersion = (isCapture ? 23 : 21);
+            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) {
+                    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
         // Let the user pick a larger buffer if they really want -- but ye
         // gods they probably shouldn't, the minimums are horrifyingly high
         // gods they probably shouldn't, the minimums are horrifyingly high
         // latency already
         // latency already
-        desiredFrames = Math.max(desiredFrames, (AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
+        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);
 
 
-        if (mAudioTrack == null) {
-            mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
-                    channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
+        int[] results = new int[4];
 
 
-            // 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 (isCapture) {
+            if (mAudioRecord == null) {
+                mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
+                        channelConfig, audioFormat, desiredFrames * frameSize);
 
 
-            if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
-                Log.e(TAG, "Failed during initialization of Audio Track");
-                mAudioTrack = null;
-                return -1;
+                // 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;
+                }
+
+                mAudioRecord.startRecording();
+            }
+
+            results[0] = mAudioRecord.getSampleRate();
+            results[1] = mAudioRecord.getAudioFormat();
+            results[2] = mAudioRecord.getChannelCount();
+            results[3] = desiredFrames;
+
+        } 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;
+                }
+
+                mAudioTrack.play();
             }
             }
 
 
-            mAudioTrack.play();
+            results[0] = mAudioTrack.getSampleRate();
+            results[1] = mAudioTrack.getAudioFormat();
+            results[2] = mAudioTrack.getChannelCount();
+            results[3] = desiredFrames;
         }
         }
 
 
-        Log.v(TAG, "SDL audio: got " + ((mAudioTrack.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioTrack.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer");
+        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;
+    }
 
 
-        return 0;
+    /**
+     * This method is called by SDL using JNI.
+     */
+    public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
+        return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames);
+    }
+
+    /**
+     * 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;
+        }
+
+        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
+                }
+            } else {
+                Log.w(TAG, "SDL audio: error return from write(float)");
+                return;
+            }
+        }
     }
     }
 
 
     /**
     /**
@@ -63,7 +273,7 @@ public class SDLAudioManager
             return;
             return;
         }
         }
 
 
-        for (int i = 0; i < buffer.length; ) {
+        for (int i = 0; i < buffer.length;) {
             int result = mAudioTrack.write(buffer, i, buffer.length - i);
             int result = mAudioTrack.write(buffer, i, buffer.length - i);
             if (result > 0) {
             if (result > 0) {
                 i += result;
                 i += result;
@@ -109,53 +319,33 @@ public class SDLAudioManager
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      */
      */
-    public static int captureOpen(int sampleRate, boolean is16Bit, boolean isStereo, int desiredFrames) {
-        int channelConfig = isStereo ? AudioFormat.CHANNEL_CONFIGURATION_STEREO : AudioFormat.CHANNEL_CONFIGURATION_MONO;
-        int audioFormat = is16Bit ? AudioFormat.ENCODING_PCM_16BIT : AudioFormat.ENCODING_PCM_8BIT;
-        int frameSize = (isStereo ? 2 : 1) * (is16Bit ? 2 : 1);
-
-        Log.v(TAG, "SDL capture: wanted " + (isStereo ? "stereo" : "mono") + " " + (is16Bit ? "16-bit" : "8-bit") + " " + (sampleRate / 1000f) + "kHz, " + desiredFrames + " frames buffer");
-
-        // 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
-        desiredFrames = Math.max(desiredFrames, (AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) + frameSize - 1) / frameSize);
-
-        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 -1;
-            }
-
-            mAudioRecord.startRecording();
-        }
-
-        Log.v(TAG, "SDL capture: got " + ((mAudioRecord.getChannelCount() >= 2) ? "stereo" : "mono") + " " + ((mAudioRecord.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) ? "16-bit" : "8-bit") + " " + (mAudioRecord.getSampleRate() / 1000f) + "kHz, " + desiredFrames + " frames buffer");
+    public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) {
+        return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames);
+    }
 
 
-        return 0;
+    /** This method is called by SDL using JNI. */
+    public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
+        return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
     }
     }
 
 
     /** This method is called by SDL using JNI. */
     /** This method is called by SDL using JNI. */
     public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
     public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
-        // !!! FIXME: this is available in API Level 23. Until then, we always block.  :(
-        //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
-        return mAudioRecord.read(buffer, 0, buffer.length);
+        if (Build.VERSION.SDK_INT < 23) {
+            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. */
     /** This method is called by SDL using JNI. */
     public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
     public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
-        // !!! FIXME: this is available in API Level 23. Until then, we always block.  :(
-        //return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
-        return mAudioRecord.read(buffer, 0, buffer.length);
+        if (Build.VERSION.SDK_INT < 23) {
+            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. */
     /** This method is called by SDL using JNI. */
     public static void audioClose() {
     public static void audioClose() {
         if (mAudioTrack != null) {
         if (mAudioTrack != null) {

+ 438 - 24
oxygine/SDL/android/lib/src/org/libsdl/app/SDLControllerManager.java

@@ -11,13 +11,14 @@ import android.view.*;
 import android.util.Log;
 import android.util.Log;
 
 
 
 
-public class SDLControllerManager 
+public class SDLControllerManager
 {
 {
 
 
     public static native int nativeSetupJNI();
     public static native int nativeSetupJNI();
 
 
     public static native int nativeAddJoystick(int device_id, String name, String desc,
     public static native int nativeAddJoystick(int device_id, String name, String desc,
-                                               int is_accelerometer, int nbuttons,
+                                               int vendor_id, int product_id,
+                                               boolean is_accelerometer, int button_mask,
                                                int naxes, int nhats, int nballs);
                                                int naxes, int nhats, int nballs);
     public static native int nativeRemoveJoystick(int device_id);
     public static native int nativeRemoveJoystick(int device_id);
     public static native int nativeAddHaptic(int device_id, String name);
     public static native int nativeAddHaptic(int device_id, String name);
@@ -42,14 +43,21 @@ public class SDLControllerManager
     }
     }
 
 
     public static void setup() {
     public static void setup() {
-        if (Build.VERSION.SDK_INT >= 16) {
+        if (Build.VERSION.SDK_INT >= 19) {
+            mJoystickHandler = new SDLJoystickHandler_API19();
+        } else if (Build.VERSION.SDK_INT >= 16) {
             mJoystickHandler = new SDLJoystickHandler_API16();
             mJoystickHandler = new SDLJoystickHandler_API16();
         } else if (Build.VERSION.SDK_INT >= 12) {
         } else if (Build.VERSION.SDK_INT >= 12) {
             mJoystickHandler = new SDLJoystickHandler_API12();
             mJoystickHandler = new SDLJoystickHandler_API12();
         } else {
         } else {
             mJoystickHandler = new SDLJoystickHandler();
             mJoystickHandler = new SDLJoystickHandler();
         }
         }
-        mHapticHandler = new SDLHapticHandler();
+
+        if (Build.VERSION.SDK_INT >= 26) {
+            mHapticHandler = new SDLHapticHandler_API26();
+        } else {
+            mHapticHandler = new SDLHapticHandler();
+        }
     }
     }
 
 
     // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
     // Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
@@ -74,8 +82,16 @@ public class SDLControllerManager
     /**
     /**
      * This method is called by SDL using JNI.
      * This method is called by SDL using JNI.
      */
      */
-    public static void hapticRun(int device_id, int length) {
-        mHapticHandler.run(device_id, length);
+    public 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)
+    {
+        mHapticHandler.stop(device_id);
     }
     }
 
 
     // Check if a given device is considered a possible SDL joystick
     // Check if a given device is considered a possible SDL joystick
@@ -141,7 +157,21 @@ class SDLJoystickHandler_API12 extends SDLJoystickHandler {
     static class RangeComparator implements Comparator<InputDevice.MotionRange> {
     static class RangeComparator implements Comparator<InputDevice.MotionRange> {
         @Override
         @Override
         public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
         public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
-            return arg0.getAxis() - arg1.getAxis();
+            // 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;
+            }
+
+            return arg0Axis - arg1Axis;
         }
         }
     }
     }
 
 
@@ -155,12 +185,7 @@ class SDLJoystickHandler_API12 extends SDLJoystickHandler {
     @Override
     @Override
     public void pollInputDevices() {
     public void pollInputDevices() {
         int[] deviceIds = InputDevice.getDeviceIds();
         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--) {
+        for(int i=0; i < deviceIds.length; ++i) {
             SDLJoystick joystick = getJoystick(deviceIds[i]);
             SDLJoystick joystick = getJoystick(deviceIds[i]);
             if (joystick == null) {
             if (joystick == null) {
                 joystick = new SDLJoystick();
                 joystick = new SDLJoystick();
@@ -187,8 +212,7 @@ class SDLJoystickHandler_API12 extends SDLJoystickHandler {
                     }
                     }
 
 
                     mJoysticks.add(joystick);
                     mJoysticks.add(joystick);
-                    SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, 0, -1,
-                                                           joystick.axes.size(), joystick.hats.size()/2, 0);
+                    SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, getVendorId(joystickDevice), getProductId(joystickDevice), false, getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0);
                 }
                 }
             }
             }
         }
         }
@@ -259,9 +283,17 @@ class SDLJoystickHandler_API12 extends SDLJoystickHandler {
     public String getJoystickDescriptor(InputDevice joystickDevice) {
     public String getJoystickDescriptor(InputDevice joystickDevice) {
         return joystickDevice.getName();
         return joystickDevice.getName();
     }
     }
+    public int getProductId(InputDevice joystickDevice) {
+        return 0;
+    }
+    public int getVendorId(InputDevice joystickDevice) {
+        return 0;
+    }
+    public int getButtonMask(InputDevice joystickDevice) {
+        return -1;
+    }
 }
 }
 
 
-
 class SDLJoystickHandler_API16 extends SDLJoystickHandler_API12 {
 class SDLJoystickHandler_API16 extends SDLJoystickHandler_API12 {
 
 
     @Override
     @Override
@@ -276,6 +308,144 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler_API12 {
     }
     }
 }
 }
 
 
+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 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_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 << 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;
+    }
+}
+
+class SDLHapticHandler_API26 extends SDLHapticHandler {
+    @Override
+    public void run(int device_id, float intensity, int length) {
+        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;
+            }
+
+            int vibeValue = Math.round(intensity * 255);
+
+            if (vibeValue > 255) {
+                vibeValue = 255;
+            }
+            if (vibeValue < 1) {
+                stop(device_id);
+                return;
+            }
+            try {
+                haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
+            }
+            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.
+                haptic.vib.vibrate(length);
+            }
+        }
+    }
+}
+
 class SDLHapticHandler {
 class SDLHapticHandler {
 
 
     class SDLHaptic {
     class SDLHaptic {
@@ -285,20 +455,27 @@ class SDLHapticHandler {
     }
     }
 
 
     private ArrayList<SDLHaptic> mHaptics;
     private ArrayList<SDLHaptic> mHaptics;
-    
+
     public SDLHapticHandler() {
     public SDLHapticHandler() {
         mHaptics = new ArrayList<SDLHaptic>();
         mHaptics = new ArrayList<SDLHaptic>();
     }
     }
 
 
-    public void run(int device_id, int length) {
+    public 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) {
         SDLHaptic haptic = getHaptic(device_id);
         SDLHaptic haptic = getHaptic(device_id);
         if (haptic != null) {
         if (haptic != null) {
-            haptic.vib.vibrate (length);
+            haptic.vib.cancel();
         }
         }
     }
     }
 
 
     public void pollHapticDevices() {
     public void pollHapticDevices() {
-        
+
         final int deviceId_VIBRATOR_SERVICE = 999999;
         final int deviceId_VIBRATOR_SERVICE = 999999;
         boolean hasVibratorService = false;
         boolean hasVibratorService = false;
 
 
@@ -342,7 +519,7 @@ class SDLHapticHandler {
                     haptic = new SDLHaptic();
                     haptic = new SDLHaptic();
                     haptic.device_id = deviceId_VIBRATOR_SERVICE;
                     haptic.device_id = deviceId_VIBRATOR_SERVICE;
                     haptic.name = "VIBRATOR_SERVICE";
                     haptic.name = "VIBRATOR_SERVICE";
-                    haptic.vib = vib; 
+                    haptic.vib = vib;
                     mHaptics.add(haptic);
                     mHaptics.add(haptic);
                     SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
                     SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
                 }
                 }
@@ -384,7 +561,7 @@ class SDLHapticHandler {
             }
             }
         }
         }
         return null;
         return null;
-    }   
+    }
 }
 }
 
 
 class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
 class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
@@ -409,14 +586,14 @@ class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
                     case MotionEvent.ACTION_SCROLL:
                     case MotionEvent.ACTION_SCROLL:
                         x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
                         x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
                         y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
                         y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                        SDLActivity.onNativeMouse(0, action, x, y);
+                        SDLActivity.onNativeMouse(0, action, x, y, false);
                         return true;
                         return true;
 
 
                     case MotionEvent.ACTION_HOVER_MOVE:
                     case MotionEvent.ACTION_HOVER_MOVE:
                         x = event.getX(0);
                         x = event.getX(0);
                         y = event.getY(0);
                         y = event.getY(0);
 
 
-                        SDLActivity.onNativeMouse(0, action, x, y);
+                        SDLActivity.onNativeMouse(0, action, x, y, false);
                         return true;
                         return true;
 
 
                     default:
                     default:
@@ -431,5 +608,242 @@ class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
         // Event was not managed
         // Event was not managed
         return false;
         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);
+    }
+
 }
 }
 
 
+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) {
+        float x, y;
+        int action;
+
+        switch ( event.getSource() ) {
+            case InputDevice.SOURCE_JOYSTICK:
+            case InputDevice.SOURCE_GAMEPAD:
+            case InputDevice.SOURCE_DPAD:
+                return SDLControllerManager.handleJoystickMotionEvent(event);
+
+            case InputDevice.SOURCE_MOUSE:
+                if (!SDLActivity.mSeparateMouseAndTouch) {
+                    break;
+                }
+                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:
+                        if (mRelativeModeEnabled) {
+                            x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
+                            y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
+                        }
+                        else {
+                            x = event.getX(0);
+                            y = event.getY(0);
+                        }
+
+                        SDLActivity.onNativeMouse(0, action, x, y, mRelativeModeEnabled);
+                        return true;
+
+                    default:
+                        break;
+                }
+                break;
+
+            default:
+                break;
+        }
+
+        // Event was not managed
+        return false;
+    }
+
+    @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:
+            case InputDevice.SOURCE_GAMEPAD:
+            case InputDevice.SOURCE_DPAD:
+                return SDLControllerManager.handleJoystickMotionEvent(event);
+
+            case InputDevice.SOURCE_MOUSE:
+            case 12290: // DeX desktop mouse cursor is a separate non-standard input type.
+                if (!SDLActivity.mSeparateMouseAndTouch) {
+                    break;
+                }
+
+                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:
+                if (!SDLActivity.mSeparateMouseAndTouch) {
+                    break;
+                }
+                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));
+    }
+
+    @Override
+    public boolean inRelativeMode() {
+        return mRelativeModeEnabled;
+    }
+
+    @Override
+    public boolean setRelativeMouseEnabled(boolean enabled) {
+        if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) {
+            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);
+    }
+}

+ 8 - 1
oxygine/src/oxygine/core/oxygine.cpp

@@ -293,6 +293,8 @@ namespace oxygine
             flags |= SDL_WINDOW_ALLOW_HIGHDPI;
             flags |= SDL_WINDOW_ALLOW_HIGHDPI;
 #elif __ANDROID__
 #elif __ANDROID__
             flags |= SDL_WINDOW_RESIZABLE;//fix for latest SDL, related with new setOrientationBis java function
             flags |= SDL_WINDOW_RESIZABLE;//fix for latest SDL, related with new setOrientationBis java function
+            flags |= SDL_WINDOW_FULLSCREEN;
+            flags |= SDL_WINDOW_BORDERLESS;
 #else
 #else
             if (desc.show_window)
             if (desc.show_window)
                 flags |= SDL_WINDOW_SHOWN;
                 flags |= SDL_WINDOW_SHOWN;
@@ -447,7 +449,8 @@ namespace oxygine
             TextField::setDefaultFont(DebugActor::resSystem->getResFont("system"));
             TextField::setDefaultFont(DebugActor::resSystem->getResFont("system"));
 #endif
 #endif
 #endif
 #endif
-            logs::messageln("oxygine initialized");
+            Point ds = getDisplaySize();
+            logs::messageln("oxygine initialized, drawable size: %d %d", ds.x, ds.y);
         }
         }
 
 
 #if OXYGINE_SDL
 #if OXYGINE_SDL
@@ -749,6 +752,10 @@ namespace oxygine
 #ifndef OXYGINE_EDITOR
 #ifndef OXYGINE_EDITOR
             key::update();
             key::update();
 #endif
 #endif
+            
+            Point ds = getDisplaySize();
+            //logs::messageln("SDL_GL_GetDrawableSize: %d %d", ds.x, ds.y);
+
             timeMS duration = IVideoDriver::_stats.duration;
             timeMS duration = IVideoDriver::_stats.duration;
             IVideoDriver::_stats = IVideoDriver::Stats();
             IVideoDriver::_stats = IVideoDriver::Stats();
             IVideoDriver::_stats.duration = duration;
             IVideoDriver::_stats.duration = duration;

BIN
oxygine/third_party/android/libraries/arm64-v8a/libjpeg.a


BIN
oxygine/third_party/android/libraries/armeabi-v7a/libjpeg.a


BIN
oxygine/third_party/android/libraries/x86/libjpeg.a