diff --git a/android/app/deps.zip b/android/app/deps.zip index a4c57d644139b..a636e2bb0c2cd 100644 Binary files a/android/app/deps.zip and b/android/app/deps.zip differ diff --git a/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java index 94a28189b8b47..ee5521fd5e3fe 100644 --- a/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ b/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -186,7 +186,7 @@ public BluetoothGatt getGatt() { // 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) { - if (Build.VERSION.SDK_INT >= 23) { + if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { try { return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE); } catch (Exception e) { @@ -429,7 +429,7 @@ public void run() { } }); } - } + } else if (newState == 0) { mIsConnected = false; } @@ -564,10 +564,10 @@ public String getProductName() { return "Steam Controller"; } - @Override + @Override public UsbDevice getDevice() { - return null; - } + return null; + } @Override public boolean open() { diff --git a/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index 56f677e660178..e7281fdf26ac4 100644 --- a/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -7,6 +7,7 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; +import android.os.Build; import android.util.Log; import android.content.BroadcastReceiver; import android.content.Context; @@ -104,36 +105,6 @@ public void onReceive(Context context, Intent intent) { private HIDDeviceManager(final Context context) { mContext = context; - // Make sure we have the HIDAPI library loaded with the native functions - try { - SDL.loadLibrary("hidapi"); - } catch (Throwable e) { - Log.w(TAG, "Couldn't load hidapi: " + e.toString()); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setCancelable(false); - builder.setTitle("SDL HIDAPI Error"); - builder.setMessage("Please report the following error to the SDL maintainers: " + e.getMessage()); - builder.setNegativeButton("Quit", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - try { - // If our context is an activity, exit rather than crashing when we can't - // call our native functions. - Activity activity = (Activity)context; - - activity.finish(); - } - catch (ClassCastException cce) { - // Context wasn't an activity, there's nothing we can do. Give up and return. - } - } - }); - builder.show(); - - return; - } - HIDDeviceRegisterCallback(); mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE); @@ -148,9 +119,6 @@ public void onClick(DialogInterface dialog, int which) { { mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); } - - initializeUSB(); - initializeBluetooth(); } public Context getContext() { @@ -173,6 +141,9 @@ public int getDeviceIDForIdentifier(String identifier) { private void initializeUSB() { mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); + if (mUsbManager == null) { + return; + } /* // Logging @@ -199,7 +170,7 @@ private void initializeUSB() { Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol()); Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount()); - // Get endpoint details + // Get endpoint details for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++) { UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi); @@ -275,8 +246,13 @@ private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterfa 0x15e4, // Numark 0x162e, // Joytech 0x1689, // Razer Onza + 0x1949, // Lab126, Inc. 0x1bad, // Harmonix + 0x20d6, // PowerA 0x24c6, // PowerA + 0x2c22, // Qanba + 0x2dc8, // 8BitDo + 0x9886, // ASTRO Gaming }; if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && @@ -297,13 +273,19 @@ private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterfa final int XB1_IFACE_SUBCLASS = 71; final int XB1_IFACE_PROTOCOL = 208; final int[] SUPPORTED_VENDORS = { + 0x03f0, // HP + 0x044f, // Thrustmaster 0x045e, // Microsoft 0x0738, // Mad Catz 0x0e6f, // PDP 0x0f0d, // Hori + 0x10f5, // Turtle Beach 0x1532, // Razer Wildcat + 0x20d6, // PowerA 0x24c6, // PowerA + 0x2dc8, // 8BitDo 0x2e24, // Hyperkin + 0x3537, // GameSir }; if (usbInterface.getId() == 0 && @@ -353,9 +335,18 @@ private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_g private void connectHIDDeviceUSB(UsbDevice usbDevice) { synchronized (this) { + int interface_mask = 0; for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) { UsbInterface usbInterface = usbDevice.getInterface(interface_index); if (isHIDDeviceInterface(usbDevice, usbInterface)) { + // Check to see if we've already added this interface + // This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive + int interface_id = usbInterface.getId(); + if ((interface_mask & (1 << interface_id)) != 0) { + continue; + } + interface_mask |= (1 << interface_id); + HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); int id = device.getId(); mDevicesById.put(id, device); @@ -368,11 +359,23 @@ private void connectHIDDeviceUSB(UsbDevice usbDevice) { private void initializeBluetooth() { Log.d(TAG, "Initializing Bluetooth"); - if (mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ && + mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT"); + return; + } + + if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ && + mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH"); return; } + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { + Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE"); + return; + } + // Find bonded bluetooth controllers and create SteamControllers for them mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { @@ -533,7 +536,7 @@ public void setFrozen(boolean frozen) { for (HIDDevice device : mDevicesById.values()) { device.setFrozen(frozen); } - } + } } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -555,6 +558,18 @@ private HIDDevice getDevice(int id) { ////////// JNI interface functions ////////////////////////////////////////////////////////////////////////////////////////////////////// + public boolean initialize(boolean usb, boolean bluetooth) { + Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); + + if (usb) { + initializeUSB(); + } + if (bluetooth) { + initializeBluetooth(); + } + return true; + } + public boolean openDevice(int deviceID) { Log.v(TAG, "openDevice deviceID=" + deviceID); HIDDevice device = getDevice(deviceID); @@ -568,7 +583,14 @@ public boolean openDevice(int deviceID) { if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) { HIDDeviceOpenPending(deviceID); try { - mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 0)); + final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31 + int flags; + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + flags = FLAG_MUTABLE; + } else { + flags = 0; + } + mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags)); } catch (Exception e) { Log.v(TAG, "Couldn't request permission for USB device " + usbDevice); HIDDeviceOpenResult(deviceID, false); diff --git a/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java index d20fe80bc692b..bfe0cf954d95b 100644 --- a/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ b/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -52,7 +52,7 @@ public int getProductId() { @Override public String getSerialNumber() { String result = null; - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { try { result = mDevice.getSerialNumber(); } @@ -74,7 +74,7 @@ public int getVersion() { @Override public String getManufacturerName() { String result = null; - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { result = mDevice.getManufacturerName(); } if (result == null) { @@ -86,7 +86,7 @@ public String getManufacturerName() { @Override public String getProductName() { String result = null; - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { result = mDevice.getProductName(); } if (result == null) { diff --git a/android/app/src/main/java/org/libsdl/app/SDL.java b/android/app/src/main/java/org/libsdl/app/SDL.java index fb7f7319a8979..44c21c1c75c95 100644 --- a/android/app/src/main/java/org/libsdl/app/SDL.java +++ b/android/app/src/main/java/org/libsdl/app/SDL.java @@ -2,7 +2,8 @@ import android.content.Context; -import java.lang.reflect.*; +import java.lang.Class; +import java.lang.reflect.Method; /** SDL library initialization @@ -28,6 +29,7 @@ public static void initialize() { // This function stores the current activity (SDL or not) public static void setContext(Context context) { + SDLAudioManager.setContext(context); mContext = context; } @@ -51,16 +53,16 @@ public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, // 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"); + 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(); + Class relinkInstanceClass = relinkInstance.getClass(); // Actually load the library! Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass); @@ -77,7 +79,7 @@ public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, catch (final SecurityException se) { throw se; } - } + } } protected static Context mContext; diff --git a/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 753acbe7cfcbe..599ec69c524f2 100644 --- a/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -1,49 +1,183 @@ package org.libsdl.app; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Hashtable; -import java.lang.reflect.Method; -import java.lang.Math; - -import android.app.*; -import android.content.*; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.UiModeManager; +import android.content.ClipboardManager; +import android.content.ClipData; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.hardware.Sensor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.Editable; import android.text.InputType; -import android.view.*; +import android.text.Selection; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.PointerIcon; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.widget.RelativeLayout; +import android.view.ViewTreeObserver; import android.widget.Button; +import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; -import android.os.*; +import android.widget.Toast; + import android.preference.PreferenceManager; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.SparseArray; -import android.graphics.*; -import android.graphics.drawable.Drawable; -import android.hardware.*; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ApplicationInfo; +import java.util.Hashtable; +import java.util.Locale; + /** SDL Activity */ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { private static final String TAG = "SDL"; + private static final int SDL_MAJOR_VERSION = 2; + private static final int SDL_MINOR_VERSION = 30; + private static final int SDL_MICRO_VERSION = 0; +/* + // Display InputType.SOURCE/CLASS of events and devices + // + // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); + // SDLActivity.debugSource(event.getSource(), "event"); + public static void debugSource(int sources, String prefix) { + int s = sources; + int s_copy = sources; + String cls = ""; + String src = ""; + int tst = 0; + int FLAG_TAINTED = 0x80000000; + + if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; + if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; + if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; + if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; + if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; + + + int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits + s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON + | InputDevice.SOURCE_CLASS_JOYSTICK + | InputDevice.SOURCE_CLASS_POINTER + | InputDevice.SOURCE_CLASS_POSITION + | InputDevice.SOURCE_CLASS_TRACKBALL); + + if (s2 != 0) cls += "Some_Unkown"; + + s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; + + if (Build.VERSION.SDK_INT >= 23) { + tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; + if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_DPAD; + if ((s & tst) == tst) src += " DPAD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_GAMEPAD; + if ((s & tst) == tst) src += " GAMEPAD"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 21) { + tst = InputDevice.SOURCE_HDMI; + if ((s & tst) == tst) src += " HDMI"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_JOYSTICK; + if ((s & tst) == tst) src += " JOYSTICK"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_KEYBOARD; + if ((s & tst) == tst) src += " KEYBOARD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_MOUSE; + if ((s & tst) == tst) src += " MOUSE"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 26) { + tst = InputDevice.SOURCE_MOUSE_RELATIVE; + if ((s & tst) == tst) src += " MOUSE_RELATIVE"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_ROTARY_ENCODER; + if ((s & tst) == tst) src += " ROTARY_ENCODER"; + s2 &= ~tst; + } + tst = InputDevice.SOURCE_STYLUS; + if ((s & tst) == tst) src += " STYLUS"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_TOUCHPAD; + if ((s & tst) == tst) src += " TOUCHPAD"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_TOUCHSCREEN; + if ((s & tst) == tst) src += " TOUCHSCREEN"; + s2 &= ~tst; + + if (Build.VERSION.SDK_INT >= 18) { + tst = InputDevice.SOURCE_TOUCH_NAVIGATION; + if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; + s2 &= ~tst; + } + + tst = InputDevice.SOURCE_TRACKBALL; + if ((s & tst) == tst) src += " TRACKBALL"; + s2 &= ~tst; + + tst = InputDevice.SOURCE_ANY; + if ((s & tst) == tst) src += " ANY"; + s2 &= ~tst; + + if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; + s2 &= ~FLAG_TAINTED; + + if (s2 != 0) src += " Some_Unkown"; + + Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); + } +*/ public static boolean mIsResumedCalled, mHasFocus; - public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24); + public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); public static boolean mAllowSDLOrientationChanges = false; // Cursor types - private static final int SDL_SYSTEM_CURSOR_NONE = -1; + // 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; @@ -64,6 +198,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; protected static int mCurrentOrientation; + protected static Locale mCurrentLocale; // Handle the state of the native layer public enum NativeState { @@ -74,12 +209,12 @@ public enum NativeState { public static NativeState mCurrentNativeState; /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ - public static boolean mBrokenLibraries; + public static boolean mBrokenLibraries = true; // Main components protected static SDLActivity mSingleton; protected static SDLSurface mSurface; - protected static View mTextEdit; + protected static DummyEdit mTextEdit; protected static boolean mScreenKeyboardShown; protected static ViewGroup mLayout; protected static SDLClipboardHandler mClipboardHandler; @@ -93,10 +228,9 @@ public enum NativeState { protected static SDLGenericMotionListener_API12 getMotionListener() { if (mMotionListener == null) { - if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { mMotionListener = new SDLGenericMotionListener_API26(); - } else - if (Build.VERSION.SDK_INT >= 24) { + } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { mMotionListener = new SDLGenericMotionListener_API24(); } else { mMotionListener = new SDLGenericMotionListener_API12(); @@ -178,12 +312,15 @@ public static void initialize() { mCursors = new Hashtable(); mLastCursorID = 0; mSDLThread = null; - mBrokenLibraries = false; mIsResumedCalled = false; mHasFocus = true; mNextNativeState = NativeState.INIT; mCurrentNativeState = NativeState.INIT; } + + protected SDLSurface createSDLSurface(Context context) { + return new SDLSurface(context); + } // Setup @Override @@ -203,6 +340,7 @@ protected void onCreate(Bundle savedInstanceState) { String errorMsgBrokenLib = ""; try { loadLibraries(); + mBrokenLibraries = false; /* success */ } catch(UnsatisfiedLinkError e) { System.err.println(e.getMessage()); mBrokenLibraries = true; @@ -213,8 +351,18 @@ protected void onCreate(Bundle savedInstanceState) { errorMsgBrokenLib = e.getMessage(); } - if (mBrokenLibraries) - { + if (!mBrokenLibraries) { + String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + + String.valueOf(SDL_MINOR_VERSION) + "." + + String.valueOf(SDL_MICRO_VERSION); + String version = nativeGetVersion(); + if (!version.equals(expected_version)) { + mBrokenLibraries = true; + errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; + } + } + + if (mBrokenLibraries) { mSingleton = this; AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." @@ -246,12 +394,12 @@ public void onClick(DialogInterface dialog,int id) { mSingleton = this; SDL.setContext(this); - mClipboardHandler = new SDLClipboardHandler_API11(); + mClipboardHandler = new SDLClipboardHandler(); mHIDDeviceManager = HIDDeviceManager.acquire(this); // Set up the surface - mSurface = new SDLSurface(getApplication()); + mSurface = createSDLSurface(this); mLayout = new RelativeLayout(this); mLayout.addView(mSurface); @@ -263,6 +411,15 @@ public void onClick(DialogInterface dialog,int id) { // Only record current orientation SDLActivity.onNativeOrientationChanged(mCurrentOrientation); + try { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { + mCurrentLocale = getContext().getResources().getConfiguration().locale; + } else { + mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); + } + } catch(Exception ignored) { + } + setContentView(mLayout); setWindowStyle(false); @@ -359,11 +516,14 @@ protected void onStart() { } public static int getCurrentOrientation() { - final Context context = SDLActivity.getContext(); - final Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - int result = SDL_ORIENTATION_UNKNOWN; + Activity activity = (Activity)getContext(); + if (activity == null) { + return result; + } + Display display = activity.getWindowManager().getDefaultDisplay(); + switch (display.getRotation()) { case Surface.ROTATION_0: result = SDL_ORIENTATION_PORTRAIT; @@ -423,6 +583,21 @@ public void onLowMemory() { SDLActivity.nativeLowMemory(); } + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.v(TAG, "onConfigurationChanged()"); + super.onConfigurationChanged(newConfig); + + if (SDLActivity.mBrokenLibraries) { + return; + } + + if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { + mCurrentLocale = newConfig.locale; + SDLActivity.onNativeLocaleChanged(); + } + } + @Override protected void onDestroy() { Log.v(TAG, "onDestroy()"); @@ -432,6 +607,8 @@ protected void onDestroy() { mHIDDeviceManager = null; } + SDLAudioManager.release(this); + if (SDLActivity.mBrokenLibraries) { super.onDestroy(); return; @@ -462,8 +639,8 @@ public void onBackPressed() { // 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")) { + boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); + if (trapBack) { // Exit and let the mouse handler handle this button (if appropriate) return; } @@ -556,11 +733,10 @@ public static void handleNativeState() { mSDLThread.start(); // No nativeResume(), don't signal Android_ResumeSem - mSurface.handleResume(); } else { nativeResume(); - mSurface.handleResume(); } + mSurface.handleResume(); mCurrentNativeState = mNextNativeState; } @@ -571,7 +747,6 @@ public static void handleNativeState() { static final int COMMAND_CHANGE_TITLE = 1; static final int COMMAND_CHANGE_WINDOW_STYLE = 2; static final int COMMAND_TEXTEDIT_HIDE = 3; - static final int COMMAND_CHANGE_SURFACEVIEW_FORMAT = 4; static final int COMMAND_SET_KEEP_SCREEN_ON = 5; protected static final int COMMAND_USER = 0x8000; @@ -612,36 +787,34 @@ public void handleMessage(Message msg) { } 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 | + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + if (context instanceof Activity) { + Window window = ((Activity) context).getWindow(); + if (window != null) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - SDLActivity.mFullscreenModeActive = true; - } else { - int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; - window.getDecorView().setSystemUiVisibility(flags); - Context appContext = context.getApplicationContext(); - boolean forceFullScreen = PreferenceManager.getDefaultSharedPreferences(appContext).getBoolean("Force fullscreen", false); - window.addFlags(forceFullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - window.clearFlags(forceFullScreen ? WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN : WindowManager.LayoutParams.FLAG_FULLSCREEN); - SDLActivity.mFullscreenModeActive = forceFullScreen; + 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); + Context appContext = context.getApplicationContext(); + boolean forceFullScreen = PreferenceManager.getDefaultSharedPreferences(appContext).getBoolean("Force fullscreen", false); + window.addFlags(forceFullScreen ? WindowManager.LayoutParams.FLAG_FULLSCREEN : WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + window.clearFlags(forceFullScreen ? WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN : WindowManager.LayoutParams.FLAG_FULLSCREEN); + SDLActivity.mFullscreenModeActive = forceFullScreen; + } } + } else { + Log.e(TAG, "error handling message, getContext() returned no Activity"); } - } else { - Log.e(TAG, "error handling message, getContext() returned no Activity"); } break; case COMMAND_TEXTEDIT_HIDE: @@ -664,7 +837,7 @@ public void handleMessage(Message msg) { if (context instanceof Activity) { Window window = ((Activity) context).getWindow(); if (window != null) { - if ((msg.obj instanceof Integer) && (((Integer) msg.obj).intValue() != 0)) { + if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -673,32 +846,6 @@ public void handleMessage(Message msg) { } break; } - case COMMAND_CHANGE_SURFACEVIEW_FORMAT: - { - int format = (Integer) msg.obj; - int pf; - - if (SDLActivity.mSurface == null) { - return; - } - - SurfaceHolder holder = SDLActivity.mSurface.getHolder(); - if (holder == null) { - return; - } - - if (format == 1) { - pf = PixelFormat.RGBA_8888; - } else if (format == 2) { - pf = PixelFormat.RGBX_8888; - } else { - pf = PixelFormat.RGB_565; - } - - holder.setFormat(pf); - - break; - } default: if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { Log.e(TAG, "error handling message, command is " + msg.arg1); @@ -717,53 +864,53 @@ boolean sendCommand(int command, Object data) { msg.obj = data; boolean result = commandHandler.sendMessage(msg); - if ((Build.VERSION.SDK_INT >= 19) && (command == COMMAND_CHANGE_WINDOW_STYLE)) { - // Ensure we don't return until the resize has actually happened, - // or 500ms have passed. - - boolean bShouldWait = false; - - if (data instanceof Integer) { - // Let's figure out if we're already laid out fullscreen or not. - Display display = ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - android.util.DisplayMetrics realMetrics = new android.util.DisplayMetrics(); - display.getRealMetrics( realMetrics ); - - boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && - (realMetrics.heightPixels == mSurface.getHeight())); - - if (((Integer)data).intValue() == 1) { - // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going - // to change size and should wait for surfaceChanged() before we return, so the size - // is right back in native code. If we're already laid out fullscreen, though, we're - // not going to change size even if we change decor modes, so we shouldn't wait for - // surfaceChanged() -- which may not even happen -- and should return immediately. - bShouldWait = !bFullscreenLayout; - } - else { - // If we're laid out fullscreen (even if the status bar and nav bar are present), - // or are actively in fullscreen, we're going to change size and should wait for - // surfaceChanged before we return, so the size is right back in native code. - bShouldWait = bFullscreenLayout; + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + if (command == COMMAND_CHANGE_WINDOW_STYLE) { + // Ensure we don't return until the resize has actually happened, + // or 500ms have passed. + + boolean bShouldWait = false; + + if (data instanceof Integer) { + // Let's figure out if we're already laid out fullscreen or not. + Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + DisplayMetrics realMetrics = new DisplayMetrics(); + display.getRealMetrics(realMetrics); + + boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && + (realMetrics.heightPixels == mSurface.getHeight())); + + if ((Integer) data == 1) { + // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going + // to change size and should wait for surfaceChanged() before we return, so the size + // is right back in native code. If we're already laid out fullscreen, though, we're + // not going to change size even if we change decor modes, so we shouldn't wait for + // surfaceChanged() -- which may not even happen -- and should return immediately. + bShouldWait = !bFullscreenLayout; + } else { + // If we're laid out fullscreen (even if the status bar and nav bar are present), + // or are actively in fullscreen, we're going to change size and should wait for + // surfaceChanged before we return, so the size is right back in native code. + bShouldWait = bFullscreenLayout; + } } - } - if (bShouldWait && (SDLActivity.getContext() != null)) { - // We'll wait for the surfaceChanged() method, which will notify us - // when called. That way, we know our current size is really the - // size we need, instead of grabbing a size that's still got - // the navigation and/or status bars before they're hidden. - // - // We'll wait for up to half a second, because some devices - // take a surprisingly long time for the surface resize, but - // then we'll just give up and return. - // - synchronized(SDLActivity.getContext()) { - try { - SDLActivity.getContext().wait(500); - } - catch (InterruptedException ie) { - ie.printStackTrace(); + if (bShouldWait && (SDLActivity.getContext() != null)) { + // We'll wait for the surfaceChanged() method, which will notify us + // when called. That way, we know our current size is really the + // size we need, instead of grabbing a size that's still got + // the navigation and/or status bars before they're hidden. + // + // We'll wait for up to half a second, because some devices + // take a surprisingly long time for the surface resize, but + // then we'll just give up and return. + // + synchronized (SDLActivity.getContext()) { + try { + SDLActivity.getContext().wait(500); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } } } } @@ -773,6 +920,7 @@ boolean sendCommand(int command, Object data) { } // C functions we call + public static native String nativeGetVersion(); public static native int nativeSetupJNI(); public static native int nativeRunMain(String library, String function, Object arguments); public static native void nativeLowMemory(); @@ -782,9 +930,9 @@ boolean sendCommand(int command, Object data) { public static native void nativeResume(); public static native void nativeFocusChanged(boolean hasFocus); public static native void onNativeDropFile(String filename); - public static native void onNativeVisibleDisplayFrameChanged(int left, int top, int right, int bottom); - public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, int format, float rate); + public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); public static native void onNativeResize(); + public static native void onNativeVisibleDisplayFrameChanged(int left, int top, int right, int bottom); public static native void onNativeKeyDown(int keycode); public static native void onNativeKeyUp(int keycode); public static native boolean onNativeSoftReturnKey(); @@ -799,10 +947,12 @@ public static native void onNativeTouch(int touchDevId, int pointerFingerId, public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); public static native String nativeGetHint(String name); + public static native boolean nativeGetHintBoolean(String name, boolean default_value); public static native void nativeSetenv(String name, String value); public static native void onNativeOrientationChanged(int orientation); public static native void nativeAddTouch(int touchId, String name); public static native void nativePermissionResult(int requestCode, boolean result); + public static native void onNativeLocaleChanged(); /** * This method is called by SDL using JNI. @@ -843,23 +993,26 @@ public void setOrientationBis(int w, int h, boolean resizable, String hint) /* If set, hint "explicitly controls which UI orientations are allowed". */ if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - } else if (hint.contains("LandscapeRight")) { - orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } else if (hint.contains("LandscapeLeft")) { + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; + } else if (hint.contains("LandscapeRight")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; } - if (hint.contains("Portrait") && hint.contains("PortraitUpsideDown")) { + /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ + boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); + + if (contains_Portrait && hint.contains("PortraitUpsideDown")) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; - } else if (hint.contains("Portrait")) { + } else if (contains_Portrait) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else if (hint.contains("PortraitUpsideDown")) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; } - boolean is_landscape_allowed = (orientation_landscape == -1 ? false : true); - boolean is_portrait_allowed = (orientation_portrait == -1 ? false : true); - int req = -1; /* Requested orientation */ + boolean is_landscape_allowed = (orientation_landscape != -1); + boolean is_portrait_allowed = (orientation_portrait != -1); + int req; /* Requested orientation */ /* No valid hint, nothing is explicitly allowed */ if (!is_portrait_allowed && !is_landscape_allowed) { @@ -891,7 +1044,7 @@ public void setOrientationBis(int w, int h, boolean resizable, String hint) } } - Log.v("SDL", "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); + Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); if (mAllowSDLOrientationChanges && req != -1) { mSingleton.setRequestedOrientation(req); } @@ -959,11 +1112,6 @@ public static boolean isScreenKeyboardShown() */ 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. // @@ -971,7 +1119,7 @@ public static boolean supportsRelativeMouse() // 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()) { + if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { return false; } @@ -997,7 +1145,7 @@ public static boolean sendMessage(int command, int param) { if (mSingleton == null) { return false; } - return mSingleton.sendCommand(command, Integer.valueOf(param)); + return mSingleton.sendCommand(command, param); } /** @@ -1021,30 +1169,30 @@ public static boolean isAndroidTV() { if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { return true; } - if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV")) { - return true; - } - return false; + return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); } - /** - * This method is called by SDL using JNI. - */ - public static boolean isTablet() { + public static double getDiagonal() + { DisplayMetrics metrics = new DisplayMetrics(); Activity activity = (Activity)getContext(); if (activity == null) { - return false; + return 0.0; } 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)); + return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); + } + /** + * This method is called by SDL using JNI. + */ + public static boolean isTablet() { // If our diagonal size is seven inches or greater, we consider ourselves a tablet. - return (dDiagonal >= 7.0); + return (getDiagonal() >= 7.0); } /** @@ -1061,12 +1209,12 @@ public static boolean isChromebook() { * This method is called by SDL using JNI. */ public static boolean isDeXMode() { - if (Build.VERSION.SDK_INT < 24) { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { return false; } try { final Configuration config = getContext().getResources().getConfiguration(); - final Class configClass = config.getClass(); + final Class configClass = config.getClass(); return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) == configClass.getField("semDesktopModeEnabled").getInt(config); } catch(Exception ignored) { @@ -1086,6 +1234,10 @@ public static DisplayMetrics getDisplayDPI() { */ public static boolean getManifestEnvironmentVariables() { try { + if (getContext() == null) { + return false; + } + ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = applicationInfo.metaData; if (bundle == null) { @@ -1103,15 +1255,14 @@ public static boolean getManifestEnvironmentVariables() { /* environment variables set! */ return true; } catch (Exception e) { - Log.v("SDL", "exception " + e.toString()); + Log.v(TAG, "exception " + e.toString()); } return false; } // This method is called by SDLControllerManager's API 26 Generic Motion Handler. - public static View getContentView() - { - return mSingleton.mLayout; + public static View getContentView() { + return mLayout; } static class ShowTextInputTask implements Runnable { @@ -1181,6 +1332,75 @@ public static boolean isTextInputEvent(KeyEvent event) { return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; } + public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { + int deviceId = event.getDeviceId(); + int source = event.getSource(); + + if (source == InputDevice.SOURCE_UNKNOWN) { + InputDevice device = InputDevice.getDevice(deviceId); + if (device != null) { + source = device.getSources(); + } + } + +// if (event.getAction() == KeyEvent.ACTION_DOWN) { +// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); +// } else if (event.getAction() == KeyEvent.ACTION_UP) { +// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); +// } + + // Dispatch the different events depending on where they come from + // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD + // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD + // + // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and + // SOURCE_JOYSTICK, while its key events arrive from the keyboard source + // So, retrieve the device itself and check all of its sources + if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { + // Note that we process events with specific key codes here + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { + return true; + } + } else if (event.getAction() == KeyEvent.ACTION_UP) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { + return true; + } + } + } + + if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { + // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses + // they are ignored here because sending them as mouse input to SDL is messy + if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + case KeyEvent.ACTION_UP: + // mark the event as handled or it will be handled by system + // handling KEYCODE_BACK by system will call onBackPressed() + return true; + } + } + } + + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (isTextInputEvent(event)) { + if (ic != null) { + ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); + } else { + SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); + } + } + onNativeKeyDown(keyCode); + return true; + } else if (event.getAction() == KeyEvent.ACTION_UP) { + onNativeKeyUp(keyCode); + return true; + } + + return false; + } + /** * This method is called by SDL using JNI. */ @@ -1191,14 +1411,6 @@ public static Surface getNativeSurface() { return SDLActivity.mSurface.getNativeSurface(); } - /** - * This method is called by SDL using JNI. - */ - public static void setSurfaceViewFormat(int format) { - mSingleton.sendCommand(COMMAND_CHANGE_SURFACEVIEW_FORMAT, format); - return; - } - // Input /** @@ -1207,82 +1419,25 @@ public static void setSurfaceViewFormat(int format) { public static void initTouch() { int[] ids = InputDevice.getDeviceIds(); - for (int i = 0; i < ids.length; ++i) { - InputDevice device = InputDevice.getDevice(ids[i]); - if (device != null && (device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) != 0) { - nativeAddTouch(device.getId(), device.getName()); - } - } - } - - // APK expansion files support - - /** com.android.vending.expansion.zipfile.ZipResourceFile object or null. */ - private static Object expansionFile; - - /** com.android.vending.expansion.zipfile.ZipResourceFile's getInputStream() or null. */ - private static Method expansionFileMethod; - - /** - * This method is called by SDL using JNI. - * @return an InputStream on success or null if no expansion file was used. - * @throws IOException on errors. Message is set for the SDL error message. - */ - public static InputStream openAPKExpansionInputStream(String fileName) throws IOException { - // Get a ZipResourceFile representing a merger of both the main and patch files - if (expansionFile == null) { - String mainHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_MAIN_FILE_VERSION"); - if (mainHint == null) { - return null; // no expansion use if no main version was set - } - String patchHint = nativeGetHint("SDL_ANDROID_APK_EXPANSION_PATCH_FILE_VERSION"); - if (patchHint == null) { - return null; // no expansion use if no patch version was set - } - - Integer mainVersion; - Integer patchVersion; - try { - mainVersion = Integer.valueOf(mainHint); - patchVersion = Integer.valueOf(patchHint); - } catch (NumberFormatException ex) { - ex.printStackTrace(); - throw new IOException("No valid file versions set for APK expansion files", ex); - } - - try { - // To avoid direct dependency on Google APK expansion library that is - // not a part of Android SDK we access it using reflection - expansionFile = Class.forName("com.android.vending.expansion.zipfile.APKExpansionSupport") - .getMethod("getAPKExpansionZipFile", Context.class, int.class, int.class) - .invoke(null, SDL.getContext(), mainVersion, patchVersion); - - expansionFileMethod = expansionFile.getClass() - .getMethod("getInputStream", String.class); - } catch (Exception ex) { - ex.printStackTrace(); - expansionFile = null; - expansionFileMethod = null; - throw new IOException("Could not access APK expansion support library", ex); + for (int id : ids) { + InputDevice device = InputDevice.getDevice(id); + /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ + if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN + || device.isVirtual())) { + + int touchDevId = device.getId(); + /* + * Prevent id to be -1, since it's used in SDL internal for synthetic events + * Appears when using Android emulator, eg: + * adb shell input mouse tap 100 100 + * adb shell input touchscreen tap 100 100 + */ + if (touchDevId < 0) { + touchDevId -= 1; + } + nativeAddTouch(touchDevId, device.getName()); } } - - // Get an input stream for a known file inside the expansion file ZIPs - InputStream fileStream; - try { - fileStream = (InputStream)expansionFileMethod.invoke(expansionFile, fileName); - } catch (Exception ex) { - // calling "getInputStream" failed - ex.printStackTrace(); - throw new IOException("Could not open stream from APK expansion file", ex); - } - - if (fileStream == null) { - // calling "getInputStream" was successful but null was returned - throw new IOException("Could not find path in APK expansion file"); - } - - return fileStream; } // Messagebox @@ -1290,9 +1445,6 @@ public static InputStream openAPKExpansionInputStream(String fileName) throws IO /** Result of current messagebox. Also used for blocking the calling thread. */ protected final int[] messageboxSelection = new int[1]; - /** Id of current dialog. */ - protected int dialogs = 0; - /** * This method is called by SDL using JNI. * Shows the messagebox from UI thread and block calling thread. @@ -1336,7 +1488,7 @@ public int messageboxShowMessageBox( runOnUiThread(new Runnable() { @Override public void run() { - showDialog(dialogs++, args); + messageboxCreateAndShow(args); } }); @@ -1356,8 +1508,7 @@ public void run() { return messageboxSelection[0]; } - @Override - protected Dialog onCreateDialog(int ignore, Bundle args) { + protected void messageboxCreateAndShow(Bundle args) { // TODO set values from "flags" to messagebox dialog @@ -1386,7 +1537,7 @@ protected Dialog onCreateDialog(int ignore, Bundle args) { // create dialog with title and a listener to wake up calling thread - final Dialog dialog = new Dialog(this); + final AlertDialog dialog = new AlertDialog.Builder(this).create(); dialog.setTitle(args.getString("title")); dialog.setCancelable(false); dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @@ -1472,7 +1623,7 @@ public void onClick(View v) { // add content to dialog and return - dialog.setContentView(content); + dialog.setView(content); dialog.setOnKeyListener(new Dialog.OnKeyListener() { @Override public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) { @@ -1487,20 +1638,22 @@ public boolean onKey(DialogInterface d, int keyCode, KeyEvent event) { } }); - return dialog; + dialog.show(); } private final Runnable rehideSystemUi = new Runnable() { @Override public void run() { - int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { + int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; - SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); + SDLActivity.this.getWindow().getDecorView().setSystemUiVisibility(flags); + } } }; @@ -1544,7 +1697,7 @@ public static int createCustomCursor(int[] colors, int width, int height, int ho Bitmap bitmap = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888); ++mLastCursorID; - if (Build.VERSION.SDK_INT >= 24) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { try { mCursors.put(mLastCursorID, PointerIcon.create(bitmap, hotSpotX, hotSpotY)); } catch (Exception e) { @@ -1556,12 +1709,25 @@ public static int createCustomCursor(int[] colors, int width, int height, int ho return mLastCursorID; } + /** + * This method is called by SDL using JNI. + */ + public static void destroyCustomCursor(int cursorID) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + try { + mCursors.remove(cursorID); + } catch (Exception e) { + } + } + return; + } + /** * This method is called by SDL using JNI. */ public static boolean setCustomCursor(int cursorID) { - if (Build.VERSION.SDK_INT >= 24) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { try { mSurface.setPointerIcon(mCursors.get(cursorID)); } catch (Exception e) { @@ -1616,7 +1782,7 @@ public static boolean setSystemCursor(int cursorID) { cursor_type = 1002; //PointerIcon.TYPE_HAND; break; } - if (Build.VERSION.SDK_INT >= 24) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { try { mSurface.setPointerIcon(PointerIcon.getSystemIcon(SDL.getContext(), cursor_type)); } catch (Exception e) { @@ -1630,7 +1796,7 @@ public static boolean setSystemCursor(int cursorID) { * This method is called by SDL using JNI. */ public static void requestPermission(String permission, int requestCode) { - if (Build.VERSION.SDK_INT < 23) { + if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { nativePermissionResult(requestCode, true); return; } @@ -1645,16 +1811,83 @@ public static void requestPermission(String permission, int requestCode) { @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - nativePermissionResult(requestCode, true); - } else { - nativePermissionResult(requestCode, false); + boolean result = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); + nativePermissionResult(requestCode, result); + } + + /** + * This method is called by SDL using JNI. + */ + public static int openURL(String url) + { + try { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(url)); + + int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; + if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { + flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + } else { + flags |= Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET; + } + i.addFlags(flags); + + mSingleton.startActivity(i); + } catch (Exception ex) { + return -1; } + return 0; } -} -/** - Simple runnable to start the SDL application + /** + * This method is called by SDL using JNI. + */ + public static int showToast(String message, int duration, int gravity, int xOffset, int yOffset) + { + if(null == mSingleton) { + return - 1; + } + + try + { + class OneShotTask implements Runnable { + String mMessage; + int mDuration; + int mGravity; + int mXOffset; + int mYOffset; + + OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) { + mMessage = message; + mDuration = duration; + mGravity = gravity; + mXOffset = xOffset; + mYOffset = yOffset; + } + + public void run() { + try + { + Toast toast = Toast.makeText(mSingleton, mMessage, mDuration); + if (mGravity >= 0) { + toast.setGravity(mGravity, mXOffset, mYOffset); + } + toast.show(); + } catch(Exception ex) { + Log.e(TAG, ex.getMessage()); + } + } + } + mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset)); + } catch(Exception ex) { + return -1; + } + return 0; + } +} + +/** + Simple runnable to start the SDL application */ class SDLMain implements Runnable { @Override @@ -1676,476 +1909,13 @@ public void run() { Log.v("SDL", "Finished main function"); - if (SDLActivity.mSingleton == null || SDLActivity.mSingleton.isFinishing()) { - // Activity is already being destroyed - } else { + if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { // Let's finish the Activity SDLActivity.mSDLThread = null; SDLActivity.mSingleton.finish(); - } - } -} - - -/** - SDLSurface. This is what we draw on, so we need to know when it's created - in order to do anything useful. - - Because of this, that's where we set up the SDL thread -*/ -class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, - View.OnKeyListener, View.OnTouchListener, SensorEventListener { - - // Sensors - protected SensorManager mSensorManager; - protected Display mDisplay; - - // Keep track of the surface size to normalize touch events - protected float mWidth, mHeight; - - // Is SurfaceView ready for rendering - public boolean mIsSurfaceReady; - - // Startup - public SDLSurface(Context context) { - super(context); - getHolder().addCallback(this); - - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - setOnKeyListener(this); - setOnTouchListener(this); - - mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); - - setOnGenericMotionListener(SDLActivity.getMotionListener()); - - // Some arbitrary defaults to avoid a potential division by zero - mWidth = 1.0f; - mHeight = 1.0f; - - mIsSurfaceReady = false; - } - - public void handlePause() { - enableSensor(Sensor.TYPE_ACCELEROMETER, false); - } - - public void handleResume() { - setFocusable(true); - setFocusableInTouchMode(true); - requestFocus(); - setOnKeyListener(this); - setOnTouchListener(this); - enableSensor(Sensor.TYPE_ACCELEROMETER, true); - } - - public Surface getNativeSurface() { - return getHolder().getSurface(); - } - - // Called when we have a valid drawing surface - @Override - public void surfaceCreated(SurfaceHolder holder) { - Log.v("SDL", "surfaceCreated()"); - SDLActivity.onNativeSurfaceCreated(); - } - - // Called when we lose the surface - @Override - public void surfaceDestroyed(SurfaceHolder holder) { - Log.v("SDL", "surfaceDestroyed()"); - - // Transition to pause, if needed - SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; - SDLActivity.handleNativeState(); - - mIsSurfaceReady = false; - SDLActivity.onNativeSurfaceDestroyed(); - } - - // Called when the surface is resized - @Override - public void surfaceChanged(SurfaceHolder holder, - int format, int width, int height) { - Log.v("SDL", "surfaceChanged()"); - - if (SDLActivity.mSingleton == null) { - return; - } - - int sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 by default - switch (format) { - case PixelFormat.RGBA_8888: - Log.v("SDL", "pixel format RGBA_8888"); - sdlFormat = 0x16462004; // SDL_PIXELFORMAT_RGBA8888 - break; - case PixelFormat.RGBX_8888: - Log.v("SDL", "pixel format RGBX_8888"); - sdlFormat = 0x16261804; // SDL_PIXELFORMAT_RGBX8888 - break; - case PixelFormat.RGB_565: - Log.v("SDL", "pixel format RGB_565"); - sdlFormat = 0x15151002; // SDL_PIXELFORMAT_RGB565 - break; - case PixelFormat.RGB_888: - Log.v("SDL", "pixel format RGB_888"); - // Not sure this is right, maybe SDL_PIXELFORMAT_RGB24 instead? - sdlFormat = 0x16161804; // SDL_PIXELFORMAT_RGB888 - break; - default: - Log.v("SDL", "pixel format unknown " + format); - break; - } - - mWidth = width; - mHeight = 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()) { - // In case we're waiting on a size change after going fullscreen, send a notification. - SDLActivity.getContext().notifyAll(); - } - - Log.v("SDL", "Window size: " + width + "x" + height); - Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight); - SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, sdlFormat, mDisplay.getRefreshRate()); - SDLActivity.onNativeResize(); - - // Prevent a screen distortion glitch, - // for instance when the device is in Landscape and a Portrait App is resumed. - boolean skip = false; - int requestedOrientation = SDLActivity.mAllowSDLOrientationChanges ? SDLActivity.mSingleton.getRequestedOrientation() : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; - - if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) - { - // Accept any - } - else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) - { - if (mWidth > mHeight) { - skip = true; - } - } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) { - if (mWidth < mHeight) { - skip = true; - } - } - - // Special Patch for Square Resolution: Black Berry Passport - if (skip) { - double min = Math.min(mWidth, mHeight); - double max = Math.max(mWidth, mHeight); - - if (max / min < 1.20) { - Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution."); - skip = false; - } - } - - // Don't skip in MultiWindow. - if (skip) { - if (Build.VERSION.SDK_INT >= 24) { - if (SDLActivity.mSingleton.isInMultiWindowMode()) { - Log.v("SDL", "Don't skip in Multi-Window"); - skip = false; - } - } - } - - if (skip) { - Log.v("SDL", "Skip .. Surface is not ready."); - mIsSurfaceReady = false; - return; - } - - /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */ - SDLActivity.onNativeSurfaceChanged(); - - /* Surface is ready */ - mIsSurfaceReady = true; - - SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED; - SDLActivity.handleNativeState(); - } - - // Key events - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - - int deviceId = event.getDeviceId(); - int source = event.getSource(); - - // Dispatch the different events depending on where they come from - // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD - // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD - // - // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and - // SOURCE_JOYSTICK, while its key events arrive from the keyboard source - // So, retrieve the device itself and check all of its sources - if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { - // Note that we process events with specific key codes here - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { - return true; - } - } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { - return true; - } - } - } - - if (source == InputDevice.SOURCE_UNKNOWN) { - InputDevice device = InputDevice.getDevice(deviceId); - if (device != null) { - source = device.getSources(); - } - } - - if ((source & InputDevice.SOURCE_KEYBOARD) != 0) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - //Log.v("SDL", "key down: " + keyCode); - if (SDLActivity.isTextInputEvent(event)) { - SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); - } - SDLActivity.onNativeKeyDown(keyCode); - return true; - } - else if (event.getAction() == KeyEvent.ACTION_UP) { - //Log.v("SDL", "key up: " + keyCode); - SDLActivity.onNativeKeyUp(keyCode); - return true; - } - } - - if ((source & InputDevice.SOURCE_MOUSE) != 0) { - // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses - // they are ignored here because sending them as mouse input to SDL is messy - if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - case KeyEvent.ACTION_UP: - // mark the event as handled or it will be handled by system - // handling KEYCODE_BACK by system will call onBackPressed() - return true; - } - } - } - - return false; - } - - // Touch events - @Override - public boolean onTouch(View v, MotionEvent event) { - /* Ref: http://developer.android.com/training/gestures/multi.html */ - final int touchDevId = event.getDeviceId(); - final int pointerCount = event.getPointerCount(); - int action = event.getActionMasked(); - int pointerFingerId; - int mouseButton; - int i = -1; - float x,y,p; - - // 12290 = Samsung DeX mode desktop mouse - // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN - // 0x2 = SOURCE_CLASS_POINTER - if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) { - try { - mouseButton = (Integer) event.getClass().getMethod("getButtonState").invoke(event); - } catch(Exception e) { - mouseButton = 1; // oh well. - } - - // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values - // if we are. We'll leverage our existing mouse motion listener - SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener(); - x = motionListener.getEventX(event); - y = motionListener.getEventY(event); - - SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode()); - } else { - switch(action) { - case MotionEvent.ACTION_MOVE: - for (i = 0; i < pointerCount; i++) { - pointerFingerId = event.getPointerId(i); - x = event.getX(i) / mWidth; - y = event.getY(i) / mHeight; - p = event.getPressure(i); - if (p > 1.0f) { - // may be larger than 1.0f on some devices - // see the documentation of getPressure(i) - p = 1.0f; - } - SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_DOWN: - // Primary pointer up/down, the index is always zero - i = 0; - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_POINTER_DOWN: - // Non primary pointer up/down - if (i == -1) { - i = event.getActionIndex(); - } - - pointerFingerId = event.getPointerId(i); - x = event.getX(i) / mWidth; - y = event.getY(i) / mHeight; - p = event.getPressure(i); - if (p > 1.0f) { - // may be larger than 1.0f on some devices - // see the documentation of getPressure(i) - p = 1.0f; - } - SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); - break; - - case MotionEvent.ACTION_CANCEL: - for (i = 0; i < pointerCount; i++) { - pointerFingerId = event.getPointerId(i); - x = event.getX(i) / mWidth; - y = event.getY(i) / mHeight; - p = event.getPressure(i); - if (p > 1.0f) { - // may be larger than 1.0f on some devices - // see the documentation of getPressure(i) - p = 1.0f; - } - SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p); - } - break; - - default: - break; - } - } - - return true; - } - - // Sensor events - public void enableSensor(int sensortype, boolean enabled) { - // TODO: This uses getDefaultSensor - what if we have >1 accels? - if (enabled) { - mSensorManager.registerListener(this, - mSensorManager.getDefaultSensor(sensortype), - SensorManager.SENSOR_DELAY_GAME, null); - } else { - mSensorManager.unregisterListener(this, - mSensorManager.getDefaultSensor(sensortype)); - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // TODO - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { - - // Since we may have an orientation set, we won't receive onConfigurationChanged events. - // We thus should check here. - int newOrientation = SDLActivity.SDL_ORIENTATION_UNKNOWN; - - float x, y; - switch (mDisplay.getRotation()) { - case Surface.ROTATION_90: - x = -event.values[1]; - y = event.values[0]; - newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE; - break; - case Surface.ROTATION_270: - x = event.values[1]; - y = -event.values[0]; - newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED; - break; - case Surface.ROTATION_180: - x = -event.values[0]; - y = -event.values[1]; - newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED; - break; - default: - x = event.values[0]; - y = event.values[1]; - newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT; - break; - } - - if (newOrientation != SDLActivity.mCurrentOrientation) { - SDLActivity.mCurrentOrientation = newOrientation; - SDLActivity.onNativeOrientationChanged(newOrientation); - } - - SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH, - y / SensorManager.GRAVITY_EARTH, - event.values[2] / SensorManager.GRAVITY_EARTH); - + } // else: Activity is already being destroyed - } } - - // 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 @@ -2168,21 +1938,7 @@ public boolean onCheckIsTextEditor() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { - /* - * This handles the hardware keyboard input - */ - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLActivity.isTextInputEvent(event)) { - ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); - return true; - } - SDLActivity.onNativeKeyDown(keyCode); - return true; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - SDLActivity.onNativeKeyUp(keyCode); - return true; - } - return false; + return SDLActivity.handleKeyEvent(v, keyCode, event, ic); } // @@ -2206,9 +1962,10 @@ public boolean onKeyPreIme (int keyCode, KeyEvent event) { public InputConnection onCreateInputConnection(EditorInfo outAttrs) { ic = new SDLInputConnection(this, true); - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; - outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI - | EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_FLAG_MULTI_LINE; + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | + EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; return ic; } @@ -2216,9 +1973,17 @@ public InputConnection onCreateInputConnection(EditorInfo outAttrs) { class SDLInputConnection extends BaseInputConnection { + protected EditText mEditText; + protected String mCommittedText = ""; + public SDLInputConnection(View targetView, boolean fullEditor) { super(targetView, fullEditor); + mEditText = new EditText(SDL.getContext()); + } + @Override + public Editable getEditable() { + return mEditText.getEditableText(); } @Override @@ -2241,100 +2006,130 @@ public boolean sendKeyEvent(KeyEvent event) { } } - return super.sendKeyEvent(event); } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == '\n') { - if (SDLActivity.onNativeSoftReturnKey()) { - return true; - } - } - nativeGenerateScancodeForUnichar(c); + if (!super.commitText(text, newCursorPosition)) { + return false; } - - SDLInputConnection.nativeCommitText(text.toString(), newCursorPosition); - - return super.commitText(text, newCursorPosition); + updateText(); + return true; } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (!super.setComposingText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } - nativeSetComposingText(text.toString(), newCursorPosition); + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { + // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection + // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 + if (beforeLength > 0 && afterLength == 0) { + // backspace(s) + while (beforeLength-- > 0) { + nativeGenerateScancodeForUnichar('\b'); + } + return true; + } + } - return super.setComposingText(text, newCursorPosition); + if (!super.deleteSurroundingText(beforeLength, afterLength)) { + return false; + } + updateText(); + return true; } - public static native void nativeCommitText(String text, int newCursorPosition); - - public native void nativeGenerateScancodeForUnichar(char c); + protected void updateText() { + final Editable content = getEditable(); + if (content == null) { + return; + } - public native void nativeSetComposingText(String text, int newCursorPosition); + String text = content.toString(); + int compareLength = Math.min(text.length(), mCommittedText.length()); + int matchLength, offset; - @Override - public boolean deleteSurroundingText(int beforeLength, int afterLength) { - // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions/14560344/android-backspace-in-webview-baseinputconnection - // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 - if (beforeLength > 0 && afterLength == 0) { - boolean ret = true; - // backspace(s) - while (beforeLength-- > 0) { - boolean ret_key = sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) - && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); - ret = ret && ret_key; + /* Backspace over characters that are no longer in the string */ + for (matchLength = 0; matchLength < compareLength; ) { + int codePoint = mCommittedText.codePointAt(matchLength); + if (codePoint != text.codePointAt(matchLength)) { + break; + } + matchLength += Character.charCount(codePoint); + } + /* FIXME: This doesn't handle graphemes, like '🌬️' */ + for (offset = matchLength; offset < mCommittedText.length(); ) { + int codePoint = mCommittedText.codePointAt(offset); + nativeGenerateScancodeForUnichar('\b'); + offset += Character.charCount(codePoint); + } + + if (matchLength < text.length()) { + String pendingText = text.subSequence(matchLength, text.length()).toString(); + for (offset = 0; offset < pendingText.length(); ) { + int codePoint = pendingText.codePointAt(offset); + if (codePoint == '\n') { + if (SDLActivity.onNativeSoftReturnKey()) { + return; + } + } + /* Higher code points don't generate simulated scancodes */ + if (codePoint < 128) { + nativeGenerateScancodeForUnichar((char)codePoint); + } + offset += Character.charCount(codePoint); } - return ret; + SDLInputConnection.nativeCommitText(pendingText, 0); } - - return super.deleteSurroundingText(beforeLength, afterLength); + mCommittedText = text; } -} - -interface SDLClipboardHandler { - public boolean clipboardHasText(); - public String clipboardGetText(); - public void clipboardSetText(String string); + public static native void nativeCommitText(String text, int newCursorPosition); + public static native void nativeGenerateScancodeForUnichar(char c); } +class SDLClipboardHandler implements + ClipboardManager.OnPrimaryClipChangedListener { -class SDLClipboardHandler_API11 implements - SDLClipboardHandler, - android.content.ClipboardManager.OnPrimaryClipChangedListener { - - protected android.content.ClipboardManager mClipMgr; + protected ClipboardManager mClipMgr; - SDLClipboardHandler_API11() { - mClipMgr = (android.content.ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE); + SDLClipboardHandler() { + mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE); mClipMgr.addPrimaryClipChangedListener(this); } - @Override public boolean clipboardHasText() { - return mClipMgr.hasText(); + return mClipMgr.hasPrimaryClip(); } - @Override public String clipboardGetText() { - CharSequence text; - text = mClipMgr.getText(); - if (text != null) { - return text.toString(); + ClipData clip = mClipMgr.getPrimaryClip(); + if (clip != null) { + ClipData.Item item = clip.getItemAt(0); + if (item != null) { + CharSequence text = item.getText(); + if (text != null) { + return text.toString(); + } + } } return null; } - @Override public void clipboardSetText(String string) { mClipMgr.removePrimaryClipChangedListener(this); - mClipMgr.setText(string); + ClipData clip = ClipData.newPlainText(null, string); + mClipMgr.setPrimaryClip(clip); mClipMgr.addPrimaryClipChangedListener(this); } @@ -2342,6 +2137,5 @@ public void clipboardSetText(String string) { public void onPrimaryClipChanged() { SDLActivity.onNativeClipboardChanged(); } - } diff --git a/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java b/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java index 0714419c2925a..7c821a4097b7c 100644 --- a/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java +++ b/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java @@ -1,37 +1,77 @@ package org.libsdl.app; -import android.media.*; +import android.content.Context; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.AudioTrack; +import android.media.MediaRecorder; import android.os.Build; import android.util.Log; -public class SDLAudioManager -{ +import java.util.Arrays; + +public class SDLAudioManager { protected static final String TAG = "SDLAudio"; protected static AudioTrack mAudioTrack; protected static AudioRecord mAudioRecord; + protected static Context mContext; + + private static final int[] NO_DEVICES = {}; + + private static AudioDeviceCallback mAudioDeviceCallback; public static void initialize() { mAudioTrack = null; mAudioRecord = null; + mAudioDeviceCallback = null; + + if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) + { + mAudioDeviceCallback = new AudioDeviceCallback() { + @Override + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId())); + } + + @Override + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId())); + } + }; + } + } + + public static void setContext(Context context) { + mContext = context; + if (context != null) { + registerAudioDeviceCallback(); + } + } + + public static void release(Context context) { + unregisterAudioDeviceCallback(context); } // Audio protected static String getAudioFormatString(int audioFormat) { switch (audioFormat) { - case AudioFormat.ENCODING_PCM_8BIT: - return "8-bit"; - case AudioFormat.ENCODING_PCM_16BIT: - return "16-bit"; - case AudioFormat.ENCODING_PCM_FLOAT: - return "float"; - default: - return Integer.toString(audioFormat); + 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) { + protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { int channelConfig; int sampleSize; int frameSize; @@ -39,10 +79,14 @@ protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, 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 (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) { if (desiredChannels > 2) { desiredChannels = 2; } + } + + /* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */ + if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) { if (sampleRate < 8000) { sampleRate = 8000; } else if (sampleRate > 48000) { @@ -51,7 +95,7 @@ protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, } if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { - int minSDKVersion = (isCapture ? 23 : 21); + int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */); if (Build.VERSION.SDK_INT < minSDKVersion) { audioFormat = AudioFormat.ENCODING_PCM_16BIT; } @@ -112,7 +156,7 @@ protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; break; case 8: - if (Build.VERSION.SDK_INT >= 23) { + if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; } else { Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); @@ -193,13 +237,16 @@ protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, return null; } + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { + mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId)); + } + mAudioRecord.startRecording(); } results[0] = mAudioRecord.getSampleRate(); results[1] = mAudioRecord.getAudioFormat(); results[2] = mAudioRecord.getChannelCount(); - results[3] = desiredFrames; } else { if (mAudioTrack == null) { @@ -217,25 +264,91 @@ protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, return null; } + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { + mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId)); + } + mAudioTrack.play(); } results[0] = mAudioTrack.getSampleRate(); results[1] = mAudioTrack.getAudioFormat(); results[2] = mAudioTrack.getChannelCount(); - results[3] = desiredFrames; } + results[3] = desiredFrames; Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); return results; } + private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) + .filter(deviceInfo -> deviceInfo.getId() == deviceId) + .findFirst() + .orElse(null); + } else { + return null; + } + } + + private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) + .filter(deviceInfo -> deviceInfo.getId() == deviceId) + .findFirst() + .orElse(null); + } else { + return null; + } + } + + private static void registerAudioDeviceCallback() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null); + } + } + + private static void unregisterAudioDeviceCallback(Context context) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback); + } + } + /** * This method is called by SDL using JNI. */ - public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { - return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames); + public static int[] getAudioOutputDevices() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray(); + } else { + return NO_DEVICES; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static int[] getAudioInputDevices() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray(); + } else { + return NO_DEVICES; + } + } + + /** + * This method is called by SDL using JNI. + */ + public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { + return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId); } /** @@ -247,6 +360,11 @@ public static void audioWriteFloatBuffer(float[] buffer) { return; } + if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) { + Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)"); + return; + } + for (int i = 0; i < buffer.length;) { int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); if (result > 0) { @@ -319,18 +437,22 @@ public static void audioWriteByteBuffer(byte[] buffer) { /** * This method is called by SDL using JNI. */ - public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { - return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames); + public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { + return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId); } /** This method is called by SDL using JNI. */ public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { + return 0; + } else { + return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); + } } /** This method is called by SDL using JNI. */ public static int captureReadShortBuffer(short[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23) { + if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { return mAudioRecord.read(buffer, 0, buffer.length); } else { return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); @@ -339,7 +461,7 @@ public static int captureReadShortBuffer(short[] buffer, boolean blocking) { /** This method is called by SDL using JNI. */ public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23) { + if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { return mAudioRecord.read(buffer, 0, buffer.length); } else { return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); @@ -384,4 +506,9 @@ public static void audioSetThreadPriority(boolean iscapture, int device_id) { } public static native int nativeSetupJNI(); + + public static native void removeAudioDevice(boolean isCapture, int deviceId); + + public static native void addAudioDevice(boolean isCapture, int deviceId); + } diff --git a/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java index a81e97bee84c1..d6913f1571f09 100644 --- a/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -6,9 +6,14 @@ import java.util.List; import android.content.Context; -import android.os.*; -import android.view.*; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.util.Log; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; public class SDLControllerManager @@ -19,7 +24,7 @@ public class SDLControllerManager public static native int nativeAddJoystick(int device_id, String name, String desc, int vendor_id, int product_id, boolean is_accelerometer, int button_mask, - int naxes, int nhats, int nballs); + int naxes, int axis_mask, int nhats, int nballs); public static native int nativeRemoveJoystick(int device_id); public static native int nativeAddHaptic(int device_id, String name); public static native int nativeRemoveHaptic(int device_id); @@ -37,7 +42,7 @@ public static native void onNativeHat(int device_id, int hat_id, public static void initialize() { if (mJoystickHandler == null) { - if (Build.VERSION.SDK_INT >= 19) { + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { mJoystickHandler = new SDLJoystickHandler_API19(); } else { mJoystickHandler = new SDLJoystickHandler_API16(); @@ -45,7 +50,7 @@ public static void initialize() { } if (mHapticHandler == null) { - if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { mHapticHandler = new SDLHapticHandler_API26(); } else { mHapticHandler = new SDLHapticHandler(); @@ -98,7 +103,7 @@ public static boolean isDeviceSDLJoystick(int deviceId) { int sources = device.getSources(); /* This is called for every button press, so let's not spam the logs */ - /** + /* if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { Log.v(TAG, "Input device " + device.getName() + " has class joystick."); } @@ -108,7 +113,7 @@ public static boolean isDeviceSDLJoystick(int deviceId) { if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { Log.v(TAG, "Input device " + device.getName() + " is a gamepad."); } - **/ + */ return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 || ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) || @@ -163,11 +168,37 @@ public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) { arg1Axis = MotionEvent.AXIS_GAS; } + // Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ. + // This is because the usual pairing are: + // - AXIS_X + AXIS_Y (left stick). + // - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers). + // - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers). + // This sorts the axes in the above order, which tends to be correct + // for Xbox-ish game pads that have the right stick on RX/RY and the + // triggers on Z/RZ. + // + // Gamepads that don't have AXIS_Z/AXIS_RZ but use + // AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this. + // + // References: + // - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input + // - https://www.kernel.org/doc/html/latest/input/gamepad.html + if (arg0Axis == MotionEvent.AXIS_Z) { + arg0Axis = MotionEvent.AXIS_RZ - 1; + } else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) { + --arg0Axis; + } + if (arg1Axis == MotionEvent.AXIS_Z) { + arg1Axis = MotionEvent.AXIS_RZ - 1; + } else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) { + --arg1Axis; + } + return arg0Axis - arg1Axis; } } - private ArrayList mJoysticks; + private final ArrayList mJoysticks; public SDLJoystickHandler_API16() { @@ -177,13 +208,14 @@ public SDLJoystickHandler_API16() { @Override public void pollInputDevices() { int[] deviceIds = InputDevice.getDeviceIds(); - for(int i=0; i < deviceIds.length; ++i) { - SDLJoystick joystick = getJoystick(deviceIds[i]); - if (joystick == null) { - joystick = new SDLJoystick(); - InputDevice joystickDevice = InputDevice.getDevice(deviceIds[i]); - if (SDLControllerManager.isDeviceSDLJoystick(deviceIds[i])) { - joystick.device_id = deviceIds[i]; + + for (int device_id : deviceIds) { + if (SDLControllerManager.isDeviceSDLJoystick(device_id)) { + SDLJoystick joystick = getJoystick(device_id); + if (joystick == null) { + InputDevice joystickDevice = InputDevice.getDevice(device_id); + joystick = new SDLJoystick(); + joystick.device_id = device_id; joystick.name = joystickDevice.getName(); joystick.desc = getJoystickDescriptor(joystickDevice); joystick.axes = new ArrayList(); @@ -191,53 +223,57 @@ public void pollInputDevices() { List ranges = joystickDevice.getMotionRanges(); Collections.sort(ranges, new RangeComparator()); - for (InputDevice.MotionRange range : ranges ) { + for (InputDevice.MotionRange range : ranges) { if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { - if (range.getAxis() == MotionEvent.AXIS_HAT_X || - range.getAxis() == MotionEvent.AXIS_HAT_Y) { + if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { joystick.hats.add(range); - } - else { + } else { joystick.axes.add(range); } } } mJoysticks.add(joystick); - SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, getVendorId(joystickDevice), getProductId(joystickDevice), false, getButtonMask(joystickDevice), joystick.axes.size(), joystick.hats.size()/2, 0); + SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, + getVendorId(joystickDevice), getProductId(joystickDevice), false, + getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0); } } } /* Check removed devices */ - ArrayList removedDevices = new ArrayList(); - for(int i=0; i < mJoysticks.size(); i++) { - int device_id = mJoysticks.get(i).device_id; - int j; - for (j=0; j < deviceIds.length; j++) { - if (device_id == deviceIds[j]) break; + ArrayList removedDevices = null; + for (SDLJoystick joystick : mJoysticks) { + int device_id = joystick.device_id; + int i; + for (i = 0; i < deviceIds.length; i++) { + if (device_id == deviceIds[i]) break; } - if (j == deviceIds.length) { - removedDevices.add(Integer.valueOf(device_id)); + if (i == deviceIds.length) { + if (removedDevices == null) { + removedDevices = new ArrayList(); + } + removedDevices.add(device_id); } } - for(int i=0; i < removedDevices.size(); i++) { - int device_id = removedDevices.get(i).intValue(); - SDLControllerManager.nativeRemoveJoystick(device_id); - for (int j=0; j < mJoysticks.size(); j++) { - if (mJoysticks.get(j).device_id == device_id) { - mJoysticks.remove(j); - break; + if (removedDevices != null) { + for (int device_id : removedDevices) { + SDLControllerManager.nativeRemoveJoystick(device_id); + for (int i = 0; i < mJoysticks.size(); i++) { + if (mJoysticks.get(i).device_id == device_id) { + mJoysticks.remove(i); + break; + } } } } } protected SDLJoystick getJoystick(int device_id) { - for(int i=0; i < mJoysticks.size(); i++) { - if (mJoysticks.get(i).device_id == device_id) { - return mJoysticks.get(i); + for (SDLJoystick joystick : mJoysticks) { + if (joystick.device_id == device_id) { + return joystick; } } return null; @@ -245,28 +281,22 @@ protected SDLJoystick getJoystick(int device_id) { @Override public boolean handleMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != 0) { - int actionPointerIndex = event.getActionIndex(); - int action = event.getActionMasked(); - switch(action) { - case MotionEvent.ACTION_MOVE: - SDLJoystick joystick = getJoystick(event.getDeviceId()); - if ( joystick != null ) { - for (int i = 0; i < joystick.axes.size(); i++) { - InputDevice.MotionRange range = joystick.axes.get(i); - /* Normalize the value to -1...1 */ - float value = ( event.getAxisValue( range.getAxis(), actionPointerIndex) - range.getMin() ) / range.getRange() * 2.0f - 1.0f; - SDLControllerManager.onNativeJoy(joystick.device_id, i, value ); - } - for (int i = 0; i < joystick.hats.size(); i+=2) { - int hatX = Math.round(event.getAxisValue( joystick.hats.get(i).getAxis(), actionPointerIndex ) ); - int hatY = Math.round(event.getAxisValue( joystick.hats.get(i+1).getAxis(), actionPointerIndex ) ); - SDLControllerManager.onNativeHat(joystick.device_id, i/2, hatX, hatY ); - } - } - break; - default: - break; + int actionPointerIndex = event.getActionIndex(); + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_MOVE) { + SDLJoystick joystick = getJoystick(event.getDeviceId()); + if (joystick != null) { + for (int i = 0; i < joystick.axes.size(); i++) { + InputDevice.MotionRange range = joystick.axes.get(i); + /* Normalize the value to -1...1 */ + float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f; + SDLControllerManager.onNativeJoy(joystick.device_id, i, value); + } + for (int i = 0; i < joystick.hats.size() / 2; i++) { + int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex)); + int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex)); + SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY); + } } } return true; @@ -287,6 +317,9 @@ public int getProductId(InputDevice joystickDevice) { public int getVendorId(InputDevice joystickDevice) { return 0; } + public int getAxisMask(List ranges) { + return -1; + } public int getButtonMask(InputDevice joystickDevice) { return -1; } @@ -304,6 +337,43 @@ public int getVendorId(InputDevice joystickDevice) { return joystickDevice.getVendorId(); } + @Override + public int getAxisMask(List ranges) { + // For compatibility, keep computing the axis mask like before, + // only really distinguishing 2, 4 and 6 axes. + int axis_mask = 0; + if (ranges.size() >= 2) { + // ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY)) + axis_mask |= 0x0003; + } + if (ranges.size() >= 4) { + // ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY)) + axis_mask |= 0x000c; + } + if (ranges.size() >= 6) { + // ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) + axis_mask |= 0x0030; + } + // Also add an indicator bit for whether the sorting order has changed. + // This serves to disable outdated gamecontrollerdb.txt mappings. + boolean have_z = false; + boolean have_past_z_before_rz = false; + for (InputDevice.MotionRange range : ranges) { + int axis = range.getAxis(); + if (axis == MotionEvent.AXIS_Z) { + have_z = true; + } else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) { + have_past_z_before_rz = true; + } + } + if (have_z && have_past_z_before_rz) { + // If both these exist, the compare() function changed sorting order. + // Set a bit to indicate this fact. + axis_mask |= 0x8000; + } + return axis_mask; + } + @Override public int getButtonMask(InputDevice joystickDevice) { int button_mask = 0; @@ -313,6 +383,7 @@ public int getButtonMask(InputDevice joystickDevice) { KeyEvent.KEYCODE_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, KeyEvent.KEYCODE_BACK, + KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_BUTTON_THUMBL, @@ -354,6 +425,7 @@ public int getButtonMask(InputDevice joystickDevice) { (1 << 2), // X -> X (1 << 3), // Y -> Y (1 << 4), // BACK -> BACK + (1 << 6), // MENU -> START (1 << 5), // MODE -> GUIDE (1 << 6), // START -> START (1 << 7), // THUMBL -> LEFTSTICK @@ -432,13 +504,13 @@ public void run(int device_id, float intensity, int length) { class SDLHapticHandler { - class SDLHaptic { + static class SDLHaptic { public int device_id; public String name; public Vibrator vib; } - private ArrayList mHaptics; + private final ArrayList mHaptics; public SDLHapticHandler() { mHaptics = new ArrayList(); @@ -504,37 +576,41 @@ public void pollHapticDevices() { } /* Check removed devices */ - ArrayList removedDevices = new ArrayList(); - for(int i=0; i < mHaptics.size(); i++) { - int device_id = mHaptics.get(i).device_id; - int j; - for (j=0; j < deviceIds.length; j++) { - if (device_id == deviceIds[j]) break; + ArrayList removedDevices = null; + for (SDLHaptic haptic : mHaptics) { + int device_id = haptic.device_id; + int i; + for (i = 0; i < deviceIds.length; i++) { + if (device_id == deviceIds[i]) break; } - if (device_id == deviceId_VIBRATOR_SERVICE && hasVibratorService) { - // don't remove the vibrator if it is still present - } else if (j == deviceIds.length) { - removedDevices.add(device_id); - } + if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) { + if (i == deviceIds.length) { + if (removedDevices == null) { + removedDevices = new ArrayList(); + } + removedDevices.add(device_id); + } + } // else: don't remove the vibrator if it is still present } - for(int i=0; i < removedDevices.size(); i++) { - int device_id = removedDevices.get(i); - SDLControllerManager.nativeRemoveHaptic(device_id); - for (int j=0; j < mHaptics.size(); j++) { - if (mHaptics.get(j).device_id == device_id) { - mHaptics.remove(j); - break; + if (removedDevices != null) { + for (int device_id : removedDevices) { + SDLControllerManager.nativeRemoveHaptic(device_id); + for (int i = 0; i < mHaptics.size(); i++) { + if (mHaptics.get(i).device_id == device_id) { + mHaptics.remove(i); + break; + } } } } } protected SDLHaptic getHaptic(int device_id) { - for(int i=0; i < mHaptics.size(); i++) { - if (mHaptics.get(i).device_id == device_id) { - return mHaptics.get(i); + for (SDLHaptic haptic : mHaptics) { + if (haptic.device_id == device_id) { + return haptic; } } return null; @@ -550,8 +626,6 @@ public boolean onGenericMotion(View v, MotionEvent event) { switch ( event.getSource() ) { case InputDevice.SOURCE_JOYSTICK: - case InputDevice.SOURCE_GAMEPAD: - case InputDevice.SOURCE_DPAD: return SDLControllerManager.handleJoystickMotionEvent(event); case InputDevice.SOURCE_MOUSE: @@ -655,8 +729,7 @@ public boolean setRelativeMouseEnabled(boolean enabled) { public float getEventX(MotionEvent event) { if (mRelativeModeEnabled) { return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X); - } - else { + } else { return event.getX(0); } } @@ -665,14 +738,12 @@ public float getEventX(MotionEvent event) { public float getEventY(MotionEvent event) { if (mRelativeModeEnabled) { return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y); - } - else { + } else { return event.getY(0); } } } - class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { // Generic Motion (mouse hover, joystick...) events go here private boolean mRelativeModeEnabled; @@ -684,8 +755,6 @@ public boolean onGenericMotion(View v, MotionEvent event) { switch ( event.getSource() ) { case InputDevice.SOURCE_JOYSTICK: - case InputDevice.SOURCE_GAMEPAD: - case InputDevice.SOURCE_DPAD: return SDLControllerManager.handleJoystickMotionEvent(event); case InputDevice.SOURCE_MOUSE: @@ -740,7 +809,7 @@ public boolean onGenericMotion(View v, MotionEvent event) { @Override public boolean supportsRelativeMouse() { - return (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)); + return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */); } @Override @@ -750,18 +819,15 @@ public boolean inRelativeMode() { @Override public boolean setRelativeMouseEnabled(boolean enabled) { - if (!SDLActivity.isDeXMode() || (Build.VERSION.SDK_INT >= 27)) { + if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) { if (enabled) { SDLActivity.getContentView().requestPointerCapture(); - } - else { + } else { SDLActivity.getContentView().releasePointerCapture(); } mRelativeModeEnabled = enabled; return true; - } - else - { + } else { return false; } } diff --git a/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/android/app/src/main/java/org/libsdl/app/SDLSurface.java new file mode 100644 index 0000000000000..09946349c9480 --- /dev/null +++ b/android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -0,0 +1,405 @@ +package org.libsdl.app; + + +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.WindowManager; + + +/** + SDLSurface. This is what we draw on, so we need to know when it's created + in order to do anything useful. + + Because of this, that's where we set up the SDL thread +*/ +public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, + View.OnKeyListener, View.OnTouchListener, SensorEventListener { + + // Sensors + protected SensorManager mSensorManager; + protected Display mDisplay; + + // Keep track of the surface size to normalize touch events + protected float mWidth, mHeight; + + // Is SurfaceView ready for rendering + public boolean mIsSurfaceReady; + + // Startup + public SDLSurface(Context context) { + super(context); + getHolder().addCallback(this); + + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + setOnKeyListener(this); + setOnTouchListener(this); + + mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + + setOnGenericMotionListener(SDLActivity.getMotionListener()); + + // Some arbitrary defaults to avoid a potential division by zero + mWidth = 1.0f; + mHeight = 1.0f; + + mIsSurfaceReady = false; + } + + public void handlePause() { + enableSensor(Sensor.TYPE_ACCELEROMETER, false); + } + + public void handleResume() { + setFocusable(true); + setFocusableInTouchMode(true); + requestFocus(); + setOnKeyListener(this); + setOnTouchListener(this); + enableSensor(Sensor.TYPE_ACCELEROMETER, true); + } + + public Surface getNativeSurface() { + return getHolder().getSurface(); + } + + // Called when we have a valid drawing surface + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.v("SDL", "surfaceCreated()"); + SDLActivity.onNativeSurfaceCreated(); + } + + // Called when we lose the surface + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.v("SDL", "surfaceDestroyed()"); + + // Transition to pause, if needed + SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; + SDLActivity.handleNativeState(); + + mIsSurfaceReady = false; + SDLActivity.onNativeSurfaceDestroyed(); + } + + // Called when the surface is resized + @Override + public void surfaceChanged(SurfaceHolder holder, + int format, int width, int height) { + Log.v("SDL", "surfaceChanged()"); + + if (SDLActivity.mSingleton == null) { + return; + } + + mWidth = width; + mHeight = height; + int nDeviceWidth = width; + int nDeviceHeight = height; + try + { + if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) { + DisplayMetrics realMetrics = new DisplayMetrics(); + mDisplay.getRealMetrics( realMetrics ); + nDeviceWidth = realMetrics.widthPixels; + nDeviceHeight = realMetrics.heightPixels; + } + } catch(Exception ignored) { + } + + synchronized(SDLActivity.getContext()) { + // In case we're waiting on a size change after going fullscreen, send a notification. + SDLActivity.getContext().notifyAll(); + } + + Log.v("SDL", "Window size: " + width + "x" + height); + Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight); + SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate()); + SDLActivity.onNativeResize(); + + // Prevent a screen distortion glitch, + // for instance when the device is in Landscape and a Portrait App is resumed. + boolean skip = false; + int requestedOrientation = SDLActivity.mAllowSDLOrientationChanges ? SDLActivity.mSingleton.getRequestedOrientation() : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + + if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) { + if (mWidth > mHeight) { + skip = true; + } + } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) { + if (mWidth < mHeight) { + skip = true; + } + } + + // Special Patch for Square Resolution: Black Berry Passport + if (skip) { + double min = Math.min(mWidth, mHeight); + double max = Math.max(mWidth, mHeight); + + if (max / min < 1.20) { + Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution."); + skip = false; + } + } + + // Don't skip in MultiWindow. + if (skip) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + if (SDLActivity.mSingleton.isInMultiWindowMode()) { + Log.v("SDL", "Don't skip in Multi-Window"); + skip = false; + } + } + } + + if (skip) { + Log.v("SDL", "Skip .. Surface is not ready."); + mIsSurfaceReady = false; + return; + } + + /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */ + SDLActivity.onNativeSurfaceChanged(); + + /* Surface is ready */ + mIsSurfaceReady = true; + + SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED; + SDLActivity.handleNativeState(); + } + + // Key events + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return SDLActivity.handleKeyEvent(v, keyCode, event, null); + } + + // Touch events + @Override + public boolean onTouch(View v, MotionEvent event) { + /* Ref: http://developer.android.com/training/gestures/multi.html */ + int touchDevId = event.getDeviceId(); + final int pointerCount = event.getPointerCount(); + int action = event.getActionMasked(); + int pointerFingerId; + int i = -1; + float x,y,p; + + /* + * Prevent id to be -1, since it's used in SDL internal for synthetic events + * Appears when using Android emulator, eg: + * adb shell input mouse tap 100 100 + * adb shell input touchscreen tap 100 100 + */ + if (touchDevId < 0) { + touchDevId -= 1; + } + + // 12290 = Samsung DeX mode desktop mouse + // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN + // 0x2 = SOURCE_CLASS_POINTER + if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) { + int mouseButton = 1; + try { + Object object = event.getClass().getMethod("getButtonState").invoke(event); + if (object != null) { + mouseButton = (Integer) object; + } + } catch(Exception ignored) { + } + + // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values + // if we are. We'll leverage our existing mouse motion listener + SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener(); + x = motionListener.getEventX(event); + y = motionListener.getEventY(event); + + SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode()); + } else { + switch(action) { + case MotionEvent.ACTION_MOVE: + for (i = 0; i < pointerCount; i++) { + pointerFingerId = event.getPointerId(i); + x = event.getX(i) / mWidth; + y = event.getY(i) / mHeight; + p = event.getPressure(i); + if (p > 1.0f) { + // may be larger than 1.0f on some devices + // see the documentation of getPressure(i) + p = 1.0f; + } + SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_DOWN: + // Primary pointer up/down, the index is always zero + i = 0; + /* fallthrough */ + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_POINTER_DOWN: + // Non primary pointer up/down + if (i == -1) { + i = event.getActionIndex(); + } + + pointerFingerId = event.getPointerId(i); + x = event.getX(i) / mWidth; + y = event.getY(i) / mHeight; + p = event.getPressure(i); + if (p > 1.0f) { + // may be larger than 1.0f on some devices + // see the documentation of getPressure(i) + p = 1.0f; + } + SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p); + break; + + case MotionEvent.ACTION_CANCEL: + for (i = 0; i < pointerCount; i++) { + pointerFingerId = event.getPointerId(i); + x = event.getX(i) / mWidth; + y = event.getY(i) / mHeight; + p = event.getPressure(i); + if (p > 1.0f) { + // may be larger than 1.0f on some devices + // see the documentation of getPressure(i) + p = 1.0f; + } + SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p); + } + break; + + default: + break; + } + } + + return true; + } + + // Sensor events + public void enableSensor(int sensortype, boolean enabled) { + // TODO: This uses getDefaultSensor - what if we have >1 accels? + if (enabled) { + mSensorManager.registerListener(this, + mSensorManager.getDefaultSensor(sensortype), + SensorManager.SENSOR_DELAY_GAME, null); + } else { + mSensorManager.unregisterListener(this, + mSensorManager.getDefaultSensor(sensortype)); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + // TODO + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + + // Since we may have an orientation set, we won't receive onConfigurationChanged events. + // We thus should check here. + int newOrientation; + + float x, y; + switch (mDisplay.getRotation()) { + case Surface.ROTATION_90: + x = -event.values[1]; + y = event.values[0]; + newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE; + break; + case Surface.ROTATION_270: + x = event.values[1]; + y = -event.values[0]; + newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED; + break; + case Surface.ROTATION_180: + x = -event.values[0]; + y = -event.values[1]; + newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED; + break; + case Surface.ROTATION_0: + default: + x = event.values[0]; + y = event.values[1]; + newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT; + break; + } + + if (newOrientation != SDLActivity.mCurrentOrientation) { + SDLActivity.mCurrentOrientation = newOrientation; + SDLActivity.onNativeOrientationChanged(newOrientation); + } + + SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH, + y / SensorManager.GRAVITY_EARTH, + event.values[2] / SensorManager.GRAVITY_EARTH); + + + } + } + + // Captured pointer events for API 26. + public boolean onCapturedPointerEvent(MotionEvent event) + { + int action = event.getActionMasked(); + + float x, y; + switch (action) { + case MotionEvent.ACTION_SCROLL: + x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0); + y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0); + SDLActivity.onNativeMouse(0, action, x, y, false); + return true; + + case MotionEvent.ACTION_HOVER_MOVE: + case MotionEvent.ACTION_MOVE: + x = event.getX(0); + y = event.getY(0); + SDLActivity.onNativeMouse(0, action, x, y, true); + return true; + + case MotionEvent.ACTION_BUTTON_PRESS: + case MotionEvent.ACTION_BUTTON_RELEASE: + + // Change our action value to what SDL's code expects. + if (action == MotionEvent.ACTION_BUTTON_PRESS) { + action = MotionEvent.ACTION_DOWN; + } else { /* MotionEvent.ACTION_BUTTON_RELEASE */ + action = MotionEvent.ACTION_UP; + } + + x = event.getX(0); + y = event.getY(0); + int button = event.getButtonState(); + + SDLActivity.onNativeMouse(button, action, x, y, true); + return true; + } + + return false; + } +} diff --git a/android/build_deps.sh b/android/build_deps.sh new file mode 100755 index 0000000000000..6902c4bdcff8f --- /dev/null +++ b/android/build_deps.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +### FIX HERE VERSION IF NECESSARY ### +### LINKS ARE AT THE END OF SCRIPT ### +SDL2_URL=https://github.com/katemonster33/SDL.git +SDL2_branch=cdda_android +SDL2_image_URL=https://github.com/libsdl-org/SDL_image.git +SDL2_image_branch=release-2.8.2 +SDL2_mixer_URL=https://github.com/libsdl-org/SDL_mixer.git +SDL2_mixer_branch=release-2.8.0 +SDL2_ttf_URL=https://github.com/libsdl-org/SDL_ttf.git +SDL2_ttf_branch=release-2.22.0 + +DEPS_ZIP_PATH=$(dir $1)/ + +# Global Variables # +NDK_DIR="/home/katie/android-ndk-r26b/" +INSTALL_DIR=$(pwd) +API="26" + +function fix_sdl_mk +{ + MK_ADDON=$'include $(CLEAR_VARS)\\\n' + MK_ADDON+=$'LOCAL_MODULE := SDL2\\\n' + MK_ADDON+=$'LOCAL_SRC_FILES := '"$(realpath ../deps/jni/SDL2/$ARCH)"$'/libSDL2.so\\\n' + MK_ADDON+=$'LOCAL_EXPORT_C_INCLUDES += '"$(realpath ../deps/jni/SDL2/include)"$'\\\n' + MK_ADDON+="include \$(PREBUILT_SHARED_LIBRARY)" + + if [[ -e tmp.mk ]]; then mv -f tmp.mk Android.mk; fi + cp -fva Android.mk tmp.mk + sed -e $'/(call my-dir)/a\\\n'"$MK_ADDON" Android.mk 1<> Android.mk +} + +function build_proj +{ + cd $1 + + if [[ ! $1 == SDL2 ]]; then + fix_sdl_mk ; + fi + + $NDK_DIR/ndk-build -C ./ \ + NDK_PROJECT_PATH=$NDK_DIR \ + APP_BUILD_SCRIPT=Android.mk \ + APP_PLATFORM=android-$API \ + APP_ABI=$ARCH $NDK_OPTIONS \ + APP_ALLOW_MISSING_DEPS=$2 \ + NDK_OUT=obj \ + NDK_LIBS_OUT=../deps/jni/$1/ + + if [[ ! $1 == SDL2 ]]; then + rm ../deps/jni/$1/$ARCH/libSDL2.so + fi + + cd .. +} + +function clone_proj +{ + if [[ ! -e "$1" ]] then + git clone "$2" -b "$3" --depth=1 "$1"; + fi +} + +################################################################################# + +NDK=$NDK_DIR/ndk-build + +if [[ ! -e "$NDK" ]]; then + echo "Can not find ndk-build in $NDK"; + exit 1; +fi + +mkdir -p build/deps +cd build/deps +rm -rf ./* +unzip ../../app/deps.zip #create deps folder +cd .. + +clone_proj SDL2 $SDL2_URL $SDL2_branch + +clone_proj SDL2_image $SDL2_image_URL $SDL2_image_branch + +clone_proj SDL2_mixer $SDL2_mixer_URL $SDL2_mixer_branch + +clone_proj SDL2_ttf $SDL2_ttf_URL $SDL2_ttf_branch + +./SDL2_mixer/external/download.sh +./SDL2_ttf/external/download.sh + +cp -f SDL2/include/*.h deps/jni/SDL2/include/ +cp -f SDL2_image/include/*.h deps/jni/SDL2_image/ +cp -f SDL2_mixer/include/*.h deps/jni/SDL2_mixer/ +cp -f SDL2_ttf/SDL_ttf.h deps/jni/SDL2_ttf/ + +for ARCH in armeabi-v7a arm64-v8a x86 x86_64 +do + build_proj SDL2 false + + build_proj SDL2_image true + + build_proj SDL2_image true + + build_proj SDL2_mixer true + + build_proj SDL2_ttf true +done +cd deps +zip ../deps.zip jni/ -r +cd ../.. + + +echo "******** DONE ********"