From 00a6a5647f460ae795846c82c3ff047a6c5b8914 Mon Sep 17 00:00:00 2001 From: Ghabry Date: Sun, 22 Dec 2024 16:10:13 +0100 Subject: [PATCH] Android: Update Android files for SDL3 --- .../main/java/org/libsdl/app/HIDDevice.java | 5 +- .../app/HIDDeviceBLESteamController.java | 49 +- .../java/org/libsdl/app/HIDDeviceManager.java | 43 +- .../java/org/libsdl/app/HIDDeviceUSB.java | 83 ++- .../app/src/main/java/org/libsdl/app/SDL.java | 8 +- .../main/java/org/libsdl/app/SDLActivity.java | 671 ++++++++++-------- .../java/org/libsdl/app/SDLAudioManager.java | 464 +----------- .../org/libsdl/app/SDLControllerManager.java | 137 ++-- .../java/org/libsdl/app/SDLDummyEdit.java | 66 ++ .../org/libsdl/app/SDLInputConnection.java | 136 ++++ .../main/java/org/libsdl/app/SDLSurface.java | 107 ++- 11 files changed, 854 insertions(+), 915 deletions(-) create mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDLDummyEdit.java create mode 100644 builds/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java index 955df5d14c..f96095324b 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java +++ b/builds/android/app/src/main/java/org/libsdl/app/HIDDevice.java @@ -13,9 +13,8 @@ interface HIDDevice public String getProductName(); public UsbDevice getDevice(); public boolean open(); - public int sendFeatureReport(byte[] report); - public int sendOutputReport(byte[] report); - public boolean getFeatureReport(byte[] report); + public int writeReport(byte[] report, boolean feature); + public boolean readReport(byte[] report, boolean feature); public void setFrozen(boolean frozen); public void close(); public void shutdown(); diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java index ee5521fd5e..a7b85d0cf8 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -457,7 +457,7 @@ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic //Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid()); if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) { - mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue()); + mManager.HIDDeviceReportResponse(getId(), characteristic.getValue()); } finishCurrentGattOperation(); @@ -575,50 +575,45 @@ public boolean open() { } @Override - public int sendFeatureReport(byte[] report) { + public int writeReport(byte[] report, boolean feature) { if (!isRegistered()) { - Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); + Log.e(TAG, "Attempted writeReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return -1; } - // We need to skip the first byte, as that doesn't go over the air - byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); - writeCharacteristic(reportCharacteristic, actual_report); - return report.length; - } - - @Override - public int sendOutputReport(byte[] report) { - if (!isRegistered()) { - Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); - if (mIsConnected) { - probeService(this); - } - return -1; + if (feature) { + // We need to skip the first byte, as that doesn't go over the air + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report)); + writeCharacteristic(reportCharacteristic, actual_report); + return report.length; + } else { + //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; } - - //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; } @Override - public boolean getFeatureReport(byte[] report) { + public boolean readReport(byte[] report, boolean feature) { if (!isRegistered()) { - Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); + Log.e(TAG, "Attempted readReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return false; } - //Log.v(TAG, "getFeatureReport"); - readCharacteristic(reportCharacteristic); - return true; + if (feature) { + readCharacteristic(reportCharacteristic); + return true; + } else { + // Not implemented + return false; + } } @Override diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index 21a1c1d18e..fe791432ff 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -193,7 +193,11 @@ private void initializeUSB() { filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION); - mContext.registerReceiver(mUsbBroadcast, filter); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED); + } else { + mContext.registerReceiver(mUsbBroadcast, filter); + } for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) { handleUsbDeviceAttached(usbDevice); @@ -404,7 +408,11 @@ private void initializeBluetooth() { IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); - mContext.registerReceiver(mBluetoothBroadcast, filter); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED); + } else { + mContext.registerReceiver(mBluetoothBroadcast, filter); + } if (mIsChromebook) { mHandler = new Handler(Looper.getMainLooper()); @@ -613,26 +621,9 @@ public boolean openDevice(int deviceID) { return false; } - public int sendOutputReport(int deviceID, byte[] report) { - try { - //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); - HIDDevice device; - device = getDevice(deviceID); - if (device == null) { - HIDDeviceDisconnected(deviceID); - return -1; - } - - return device.sendOutputReport(report); - } catch (Exception e) { - Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); - } - return -1; - } - - public int sendFeatureReport(int deviceID, byte[] report) { + public int writeReport(int deviceID, byte[] report, boolean feature) { try { - //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); + //Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length); HIDDevice device; device = getDevice(deviceID); if (device == null) { @@ -640,16 +631,16 @@ public int sendFeatureReport(int deviceID, byte[] report) { return -1; } - return device.sendFeatureReport(report); + return device.writeReport(report, feature); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } return -1; } - public boolean getFeatureReport(int deviceID, byte[] report) { + public boolean readReport(int deviceID, byte[] report, boolean feature) { try { - //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); + //Log.v(TAG, "readReport deviceID=" + deviceID); HIDDevice device; device = getDevice(deviceID); if (device == null) { @@ -657,7 +648,7 @@ public boolean getFeatureReport(int deviceID, byte[] report) { return false; } - return device.getFeatureReport(report); + return device.readReport(report, feature); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } @@ -694,5 +685,5 @@ public void closeDevice(int deviceID) { native void HIDDeviceDisconnected(int deviceID); native void HIDDeviceInputReport(int deviceID, byte[] report); - native void HIDDeviceFeatureReport(int deviceID, byte[] report); + native void HIDDeviceReportResponse(int deviceID, byte[] report); } diff --git a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java index bfe0cf954d..27414386df 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ b/builds/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -153,55 +153,64 @@ public boolean open() { } @Override - public int sendFeatureReport(byte[] report) { - int res = -1; - int offset = 0; - int length = report.length; - boolean skipped_report_id = false; - byte report_number = report[0]; - - if (report_number == 0x0) { - ++offset; - --length; - skipped_report_id = true; - } - - res = mConnection.controlTransfer( - UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, - 0x09/*HID set_report*/, - (3/*HID feature*/ << 8) | report_number, - mInterface, - report, offset, length, - 1000/*timeout millis*/); - - if (res < 0) { - Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); + public int writeReport(byte[] report, boolean feature) { + if (mConnection == null) { + Log.w(TAG, "writeReport() called with no device connection"); return -1; } - if (skipped_report_id) { - ++length; - } - return length; - } + if (feature) { + int res = -1; + int offset = 0; + int length = report.length; + boolean skipped_report_id = false; + byte report_number = report[0]; + + if (report_number == 0x0) { + ++offset; + --length; + skipped_report_id = true; + } - @Override - public int sendOutputReport(byte[] report) { - int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); - if (r != report.length) { - Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); + res = mConnection.controlTransfer( + UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT, + 0x09/*HID set_report*/, + (3/*HID feature*/ << 8) | report_number, + mInterface, + report, offset, length, + 1000/*timeout millis*/); + + if (res < 0) { + Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName()); + return -1; + } + + if (skipped_report_id) { + ++length; + } + return length; + } else { + int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); + if (res != report.length) { + Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName()); + } + return res; } - return r; } @Override - public boolean getFeatureReport(byte[] report) { + public boolean readReport(byte[] report, boolean feature) { int res = -1; int offset = 0; int length = report.length; boolean skipped_report_id = false; byte report_number = report[0]; + if (mConnection == null) { + Log.w(TAG, "readReport() called with no device connection"); + return false; + } + if (report_number == 0x0) { /* Offset the return buffer by 1, so that the report ID will remain in byte 0. */ @@ -213,7 +222,7 @@ public boolean getFeatureReport(byte[] report) { res = mConnection.controlTransfer( UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN, 0x01/*HID get_report*/, - (3/*HID feature*/ << 8) | report_number, + ((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number, mInterface, report, offset, length, 1000/*timeout millis*/); @@ -234,7 +243,7 @@ public boolean getFeatureReport(byte[] report) { } else { data = Arrays.copyOfRange(report, 0, res); } - mManager.HIDDeviceFeatureReport(mDeviceId, data); + mManager.HIDDeviceReportResponse(mDeviceId, data); return true; } diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDL.java b/builds/android/app/src/main/java/org/libsdl/app/SDL.java index 139be9d151..b132fea088 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/SDL.java +++ b/builds/android/app/src/main/java/org/libsdl/app/SDL.java @@ -48,13 +48,13 @@ public static void loadLibrary(String libraryName, Context context) throws Unsat } try { - // Let's see if we have ReLinker available in the project. This is necessary for - // some projects that have huge numbers of local libraries bundled, and thus may + // Let's see if we have ReLinker available in the project. This is necessary for + // some projects that have huge numbers of local libraries bundled, and thus may // trip a bug in Android's native library loader which ReLinker works around. (If // loadLibrary works properly, ReLinker will simply use the normal Android method // internally.) // - // To use ReLinker, just add it as a dependency. For more information, see + // To use ReLinker, just add it as a dependency. For more information, see // https://github.com/KeepSafe/ReLinker for ReLinker's repository. // Class relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker"); @@ -62,7 +62,7 @@ public static void loadLibrary(String libraryName, Context context) throws Unsat Class contextClass = context.getClassLoader().loadClass("android.content.Context"); Class stringClass = context.getClassLoader().loadClass("java.lang.String"); - // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if + // Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if // they've changed during updates. Method forceMethod = relinkClass.getDeclaredMethod("force"); Object relinkInstance = forceMethod.invoke(null); diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 77053ba718..339822abc0 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/builds/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -4,6 +4,7 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.UiModeManager; +import android.content.ActivityNotFoundException; import android.content.ClipboardManager; import android.content.ClipData; import android.content.Context; @@ -23,9 +24,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.text.Editable; -import android.text.InputType; -import android.text.Selection; +import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; @@ -39,17 +38,17 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.inputmethod.BaseInputConnection; -import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; import android.widget.Button; -import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.Hashtable; import java.util.Locale; @@ -61,9 +60,9 @@ */ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { private static final String TAG = "SDL"; - private static final int SDL_MAJOR_VERSION = 2; - private static final int SDL_MINOR_VERSION = 30; - private static final int SDL_MICRO_VERSION = 6; + private static final int SDL_MAJOR_VERSION = 3; + private static final int SDL_MINOR_VERSION = 1; + private static final int SDL_MICRO_VERSION = 7; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -91,7 +90,7 @@ public static void debugSource(int sources, String prefix) { | InputDevice.SOURCE_CLASS_POSITION | InputDevice.SOURCE_CLASS_TRACKBALL); - if (s2 != 0) cls += "Some_Unkown"; + if (s2 != 0) cls += "Some_Unknown"; s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; @@ -165,7 +164,7 @@ public static void debugSource(int sources, String prefix) { if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; s2 &= ~FLAG_TAINTED; - if (s2 != 0) src += " Some_Unkown"; + if (s2 != 0) src += " Some_Unknown"; Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); } @@ -188,6 +187,14 @@ public static void debugSource(int sources, String prefix) { private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; private static final int SDL_SYSTEM_CURSOR_NO = 10; private static final int SDL_SYSTEM_CURSOR_HAND = 11; + private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT = 12; + private static final int SDL_SYSTEM_CURSOR_WINDOW_TOP = 13; + private static final int SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT = 14; + private static final int SDL_SYSTEM_CURSOR_WINDOW_RIGHT = 15; + private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT = 16; + private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOM = 17; + private static final int SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT = 18; + private static final int SDL_SYSTEM_CURSOR_WINDOW_LEFT = 19; protected static final int SDL_ORIENTATION_UNKNOWN = 0; protected static final int SDL_ORIENTATION_LANDSCAPE = 1; @@ -195,7 +202,7 @@ public static void debugSource(int sources, String prefix) { protected static final int SDL_ORIENTATION_PORTRAIT = 3; protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; - protected static int mCurrentOrientation; + protected static int mCurrentRotation; protected static Locale mCurrentLocale; // Handle the state of the native layer @@ -212,7 +219,7 @@ public enum NativeState { // Main components protected static SDLActivity mSingleton; protected static SDLSurface mSurface; - protected static DummyEdit mTextEdit; + protected static SDLDummyEdit mTextEdit; protected static boolean mScreenKeyboardShown; protected static ViewGroup mLayout; protected static SDLClipboardHandler mClipboardHandler; @@ -223,6 +230,9 @@ public enum NativeState { // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; + protected static boolean mSDLMainFinished = false; + protected static boolean mActivityCreated = false; + private static SDLFileDialogState mFileDialogState = null; protected static SDLGenericMotionListener_API12 getMotionListener() { if (mMotionListener == null) { @@ -238,6 +248,22 @@ protected static SDLGenericMotionListener_API12 getMotionListener() { return mMotionListener; } + /** + * The application entry point, called on a dedicated thread (SDLThread). + * The default implementation uses the getMainSharedObject() and getMainFunction() methods + * to invoke native code from the specified shared library. + * It can be overridden by derived classes. + */ + protected void main() { + String library = SDLActivity.mSingleton.getMainSharedObject(); + String function = SDLActivity.mSingleton.getMainFunction(); + String[] arguments = SDLActivity.mSingleton.getArguments(); + + Log.v("SDL", "Running main function " + function + " from library " + library); + SDLActivity.nativeRunMain(library, function, arguments); + Log.v("SDL", "Finished main function"); + } + /** * This method returns the name of the shared object with the application entry point * It can be overridden by derived classes. @@ -265,17 +291,17 @@ protected String getMainFunction() { * This method is called by SDL before loading the native shared libraries. * It can be overridden to provide names of shared libraries to be loaded. * The default implementation returns the defaults. It never returns null. - * An array returned by a new implementation must at least contain "SDL2". + * An array returned by a new implementation must at least contain "SDL3". * Also keep in mind that the order the libraries are loaded may matter. - * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). + * @return names of shared libraries to be loaded (e.g. "SDL3", "main"). */ protected String[] getLibraries() { return new String[] { - "SDL2", - // "SDL2_image", - // "SDL2_mixer", - // "SDL2_net", - // "SDL2_ttf", + "SDL3", + // "SDL3_image", + // "SDL3_mixer", + // "SDL3_net", + // "SDL3_ttf", "main" }; } @@ -313,7 +339,7 @@ public static void initialize() { mNextNativeState = NativeState.INIT; mCurrentNativeState = NativeState.INIT; } - + protected SDLSurface createSDLSurface(Context context) { return new SDLSurface(context); } @@ -321,11 +347,30 @@ protected SDLSurface createSDLSurface(Context context) { // Setup @Override protected void onCreate(Bundle savedInstanceState) { + Log.v(TAG, "Manufacturer: " + Build.MANUFACTURER); Log.v(TAG, "Device: " + Build.DEVICE); Log.v(TAG, "Model: " + Build.MODEL); Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); + + /* Control activity re-creation */ + if (mSDLMainFinished || mActivityCreated) { + boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); + if (mSDLMainFinished) { + Log.v(TAG, "SDL main() finished"); + } + if (allow_recreate) { + Log.v(TAG, "activity re-created"); + } else { + Log.v(TAG, "activity finished"); + System.exit(0); + return; + } + } + + mActivityCreated = true; + try { Thread.currentThread().setName("SDLActivity"); } catch (Exception e) { @@ -383,6 +428,24 @@ public void onClick(DialogInterface dialog,int id) { return; } + + /* Control activity re-creation */ + /* Robustness: check that the native code is run for the first time. + * (Maybe Activity was reset, but not the native code.) */ + { + int run_count = SDLActivity.nativeCheckSDLThreadCounter(); /* get and increment a native counter */ + if (run_count != 0) { + boolean allow_recreate = SDLActivity.nativeAllowRecreateActivity(); + if (allow_recreate) { + Log.v(TAG, "activity re-created // run_count: " + run_count); + } else { + Log.v(TAG, "activity finished // run_count: " + run_count); + System.exit(0); + return; + } + } + } + // Set up JNI SDL.setupJNI(); @@ -406,9 +469,9 @@ public void onClick(DialogInterface dialog,int id) { */ // Get our current screen orientation and pass it down. - mCurrentOrientation = SDLActivity.getCurrentOrientation(); - // Only record current orientation - SDLActivity.onNativeOrientationChanged(mCurrentOrientation); + SDLActivity.nativeSetNaturalOrientation(SDLActivity.getNaturalOrientation()); + mCurrentRotation = SDLActivity.getCurrentRotation(); + SDLActivity.onNativeRotationChanged(mCurrentRotation); try { if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { @@ -419,6 +482,15 @@ public void onClick(DialogInterface dialog,int id) { } catch(Exception ignored) { } + switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { + case Configuration.UI_MODE_NIGHT_NO: + SDLActivity.onNativeDarkModeChanged(false); + break; + case Configuration.UI_MODE_NIGHT_YES: + SDLActivity.onNativeDarkModeChanged(true); + break; + } + /* EasyRPG modification: overwrite layout setContentView(mLayout); */ @@ -505,33 +577,47 @@ protected void onStart() { } } - public static int getCurrentOrientation() { + public static int getNaturalOrientation() { int result = SDL_ORIENTATION_UNKNOWN; Activity activity = (Activity)getContext(); - if (activity == null) { - return result; - } - Display display = activity.getWindowManager().getDefaultDisplay(); - - switch (display.getRotation()) { - case Surface.ROTATION_0: - result = SDL_ORIENTATION_PORTRAIT; - break; - - case Surface.ROTATION_90: + if (activity != null) { + Configuration config = activity.getResources().getConfiguration(); + Display display = activity.getWindowManager().getDefaultDisplay(); + int rotation = display.getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) && + config.orientation == Configuration.ORIENTATION_LANDSCAPE) || + ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) && + config.orientation == Configuration.ORIENTATION_PORTRAIT)) { result = SDL_ORIENTATION_LANDSCAPE; - break; + } else { + result = SDL_ORIENTATION_PORTRAIT; + } + } + return result; + } - case Surface.ROTATION_180: - result = SDL_ORIENTATION_PORTRAIT_FLIPPED; - break; + public static int getCurrentRotation() { + int result = 0; - case Surface.ROTATION_270: - result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; - break; + Activity activity = (Activity)getContext(); + if (activity != null) { + Display display = activity.getWindowManager().getDefaultDisplay(); + switch (display.getRotation()) { + case Surface.ROTATION_0: + result = 0; + break; + case Surface.ROTATION_90: + result = 90; + break; + case Surface.ROTATION_180: + result = 180; + break; + case Surface.ROTATION_270: + result = 270; + break; + } } - return result; } @@ -562,9 +648,9 @@ public void onWindowFocusChanged(boolean hasFocus) { } @Override - public void onLowMemory() { - Log.v(TAG, "onLowMemory()"); - super.onLowMemory(); + public void onTrimMemory(int level) { + Log.v(TAG, "onTrimMemory()"); + super.onTrimMemory(level); if (SDLActivity.mBrokenLibraries) { return; @@ -586,6 +672,15 @@ public void onConfigurationChanged(Configuration newConfig) { mCurrentLocale = newConfig.locale; SDLActivity.onNativeLocaleChanged(); } + + switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) { + case Configuration.UI_MODE_NIGHT_NO: + SDLActivity.onNativeDarkModeChanged(false); + break; + case Configuration.UI_MODE_NIGHT_YES: + SDLActivity.onNativeDarkModeChanged(true); + break; + } } @Override @@ -611,7 +706,11 @@ protected void onDestroy() { // Wait for "SDLThread" thread to end try { - SDLActivity.mSDLThread.join(); + // Use a timeout because: + // C SDLmain() thread might have started (mSDLThread.start() called) + // while the SDL_Init() might not have been called yet, + // and so the previous QUIT event will be discarded by SDL_Init() and app is running, not exiting. + SDLActivity.mSDLThread.join(1000); } catch(Exception e) { Log.v(TAG, "Problem stopping SDLThread: " + e); } @@ -641,6 +740,43 @@ public void onBackPressed() { } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (mFileDialogState != null && mFileDialogState.requestCode == requestCode) { + /* This is our file dialog */ + String[] filelist = null; + + if (data != null) { + Uri singleFileUri = data.getData(); + + if (singleFileUri == null) { + /* Use Intent.getClipData to get multiple choices */ + ClipData clipData = data.getClipData(); + assert clipData != null; + + filelist = new String[clipData.getItemCount()]; + + for (int i = 0; i < filelist.length; i++) { + String uri = clipData.getItemAt(i).getUri().toString(); + filelist[i] = uri; + } + } else { + /* Only one file is selected. */ + filelist = new String[]{singleFileUri.toString()}; + } + } else { + /* User cancelled the request. */ + filelist = new String[0]; + } + + // TODO: Detect the file MIME type and pass the filter value accordingly. + SDLActivity.onNativeFileDialog(requestCode, filelist, -1); + mFileDialogState = null; + } + } + // Called by JNI from SDL. public static void manualBackButton() { mSingleton.pressBackButton(); @@ -712,7 +848,7 @@ public static void handleNativeState() { // Try a transition to resumed state if (mNextNativeState == NativeState.RESUMED) { - if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { + if (mSurface.mIsSurfaceReady && (mHasFocus || mHasMultiWindow) && mIsResumedCalled) { if (mSDLThread == null) { // This is the entry point to the C app. // Start up the C app thread and enable sensor input for the first time @@ -734,11 +870,10 @@ public static void handleNativeState() { } // Messages from the SDLMain thread - static final int COMMAND_CHANGE_TITLE = 1; - static final int COMMAND_CHANGE_WINDOW_STYLE = 2; - static final int COMMAND_TEXTEDIT_HIDE = 3; - static final int COMMAND_SET_KEEP_SCREEN_ON = 5; - + protected static final int COMMAND_CHANGE_TITLE = 1; + protected static final int COMMAND_CHANGE_WINDOW_STYLE = 2; + protected static final int COMMAND_TEXTEDIT_HIDE = 3; + protected static final int COMMAND_SET_KEEP_SCREEN_ON = 5; protected static final int COMMAND_USER = 0x8000; protected static boolean mFullscreenModeActive; @@ -799,6 +934,9 @@ public void handleMessage(Message msg) { window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); SDLActivity.mFullscreenModeActive = false; } + if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) { + window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + } } } else { Log.e(TAG, "error handling message, getContext() returned no Activity"); @@ -846,7 +984,7 @@ public void handleMessage(Message msg) { Handler commandHandler = new SDLCommandHandler(); // Send a message from the SDLMain thread - boolean sendCommand(int command, Object data) { + protected boolean sendCommand(int command, Object data) { Message msg = commandHandler.obtainMessage(); msg.arg1 = command; msg.obj = data; @@ -910,6 +1048,8 @@ boolean sendCommand(int command, Object data) { // C functions we call public static native String nativeGetVersion(); public static native int nativeSetupJNI(); + public static native void nativeInitMainThread(); + public static native void nativeCleanupMainThread(); public static native int nativeRunMain(String library, String function, Object arguments); public static native void nativeLowMemory(); public static native void nativeSendQuit(); @@ -918,7 +1058,7 @@ boolean sendCommand(int command, Object data) { public static native void nativeResume(); public static native void nativeFocusChanged(boolean hasFocus); public static native void onNativeDropFile(String filename); - public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); + public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float density, float rate); public static native void onNativeResize(); public static native void onNativeKeyDown(int keycode); public static native void onNativeKeyUp(int keycode); @@ -936,10 +1076,16 @@ public static native void onNativeTouch(int touchDevId, int pointerFingerId, public static native String nativeGetHint(String name); public static native boolean nativeGetHintBoolean(String name, boolean default_value); public static native void nativeSetenv(String name, String value); - public static native void onNativeOrientationChanged(int orientation); + public static native void nativeSetNaturalOrientation(int orientation); + public static native void onNativeRotationChanged(int rotation); + public static native void onNativeInsetsChanged(int left, int right, int top, int bottom); public static native void nativeAddTouch(int touchId, String name); public static native void nativePermissionResult(int requestCode, boolean result); public static native void onNativeLocaleChanged(); + public static native void onNativeDarkModeChanged(boolean enabled); + public static native boolean nativeAllowRecreateActivity(); + public static native int nativeCheckSDLThreadCounter(); + public static native void onNativeFileDialog(int requestCode, String[] filelist, int filter); /** * This method is called by SDL using JNI. @@ -979,7 +1125,7 @@ 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; + orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; } else if (hint.contains("LandscapeLeft")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } else if (hint.contains("LandscapeRight")) { @@ -990,7 +1136,7 @@ public void setOrientationBis(int w, int h, boolean resizable, String hint) boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); if (contains_Portrait && hint.contains("PortraitUpsideDown")) { - orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; + orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; } else if (contains_Portrait) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else if (hint.contains("PortraitUpsideDown")) { @@ -1054,23 +1200,6 @@ public static void minimizeWindow() { * This method is called by SDL using JNI. */ public static boolean shouldMinimizeOnFocusLoss() { -/* - if (Build.VERSION.SDK_INT >= 24) { - if (mSingleton == null) { - return true; - } - - if (mSingleton.isInMultiWindowMode()) { - return false; - } - - if (mSingleton.isInPictureInPictureMode()) { - return false; - } - } - - return true; -*/ return false; } @@ -1154,7 +1283,20 @@ public static boolean isAndroidTV() { if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { return true; } - return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); + if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV")) { + return true; + } + return false; + } + + public static boolean isVRHeadset() { + if (Build.MANUFACTURER.equals("Oculus") && Build.MODEL.startsWith("Quest")) { + return true; + } + if (Build.MANUFACTURER.equals("Pico")) { + return true; + } + return false; } public static double getDiagonal() @@ -1207,13 +1349,6 @@ public static boolean isDeXMode() { } } - /** - * This method is called by SDL using JNI. - */ - public static DisplayMetrics getDisplayDPI() { - return getContext().getResources().getDisplayMetrics(); - } - /** * This method is called by SDL using JNI. */ @@ -1258,9 +1393,11 @@ static class ShowTextInputTask implements Runnable { */ static final int HEIGHT_PADDING = 15; + public int input_type; public int x, y, w, h; - public ShowTextInputTask(int x, int y, int w, int h) { + public ShowTextInputTask(int input_type, int x, int y, int w, int h) { + this.input_type = input_type; this.x = x; this.y = y; this.w = w; @@ -1282,12 +1419,13 @@ public void run() { params.topMargin = y; if (mTextEdit == null) { - mTextEdit = new DummyEdit(SDL.getContext()); + mTextEdit = new SDLDummyEdit(SDL.getContext()); mLayout.addView(mTextEdit, params); } else { mTextEdit.setLayoutParams(params); } + mTextEdit.setInputType(input_type); mTextEdit.setVisibility(View.VISIBLE); mTextEdit.requestFocus(); @@ -1302,9 +1440,9 @@ public void run() { /** * This method is called by SDL using JNI. */ - public static boolean showTextInput(int x, int y, int w, int h) { + public static boolean showTextInput(int input_type, int x, int y, int w, int h) { // Transfer the task to the main thread as a Runnable - return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); + return mSingleton.commandHandler.post(new ShowTextInputTask(input_type, x, y, w, h)); } public static boolean isTextInputEvent(KeyEvent event) { @@ -1351,26 +1489,31 @@ public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputC if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode)) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode)) { return true; } } } if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { - // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses - // they are ignored here because sending them as mouse input to SDL is messy - if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { - switch (event.getAction()) { - case KeyEvent.ACTION_DOWN: - case KeyEvent.ACTION_UP: - // mark the event as handled or it will be handled by system - // handling KEYCODE_BACK by system will call onBackPressed() - return true; + if (SDLActivity.isVRHeadset()) { + // The Oculus Quest controller back button comes in as source mouse, so accept that + } else { + // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses + // they are ignored here because sending them as mouse input to SDL is messy + if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { + Log.v("SDL", "keycode is back or 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; + } } } } @@ -1417,17 +1560,7 @@ public static void initTouch() { if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN || device.isVirtual())) { - int touchDevId = device.getId(); - /* - * Prevent id to be -1, since it's used in SDL internal for synthetic events - * Appears when using Android emulator, eg: - * adb shell input mouse tap 100 100 - * adb shell input touchscreen tap 100 100 - */ - if (touchDevId < 0) { - touchDevId -= 1; - } - nativeAddTouch(touchDevId, device.getName()); + nativeAddTouch(device.getId(), device.getName()); } } } @@ -1773,6 +1906,30 @@ public static boolean setSystemCursor(int cursorID) { case SDL_SYSTEM_CURSOR_HAND: cursor_type = 1002; //PointerIcon.TYPE_HAND; break; + case SDL_SYSTEM_CURSOR_WINDOW_TOPLEFT: + cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_TOP: + cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_TOPRIGHT: + cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_RIGHT: + cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMRIGHT: + cursor_type = 1017; //PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_BOTTOM: + cursor_type = 1015; //PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_BOTTOMLEFT: + cursor_type = 1016; //PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; + break; + case SDL_SYSTEM_CURSOR_WINDOW_LEFT: + cursor_type = 1014; //PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; + break; } if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { try { @@ -1810,7 +1967,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in /** * This method is called by SDL using JNI. */ - public static int openURL(String url) + public static boolean openURL(String url) { try { Intent i = new Intent(Intent.ACTION_VIEW); @@ -1826,28 +1983,28 @@ public static int openURL(String url) mSingleton.startActivity(i); } catch (Exception ex) { - return -1; + return false; } - return 0; + return true; } /** * This method is called by SDL using JNI. */ - public static int showToast(String message, int duration, int gravity, int xOffset, int yOffset) + public static boolean showToast(String message, int duration, int gravity, int xOffset, int yOffset) { if(null == mSingleton) { - return - 1; + return false; } try { class OneShotTask implements Runnable { - String mMessage; - int mDuration; - int mGravity; - int mXOffset; - int mYOffset; + private final String mMessage; + private final int mDuration; + private final int mGravity; + private final int mXOffset; + private final int mYOffset; OneShotTask(String message, int duration, int gravity, int xOffset, int yOffset) { mMessage = message; @@ -1872,222 +2029,124 @@ public void run() { } mSingleton.runOnUiThread(new OneShotTask(message, duration, gravity, xOffset, yOffset)); } catch(Exception ex) { - return -1; + return false; } - return 0; + return true; } -} -/** - Simple runnable to start the SDL application -*/ -class SDLMain implements Runnable { - @Override - public void run() { - // Runs SDL_main() - String library = SDLActivity.mSingleton.getMainSharedObject(); - String function = SDLActivity.mSingleton.getMainFunction(); - String[] arguments = SDLActivity.mSingleton.getArguments(); + /** + * This method is called by SDL using JNI. + */ + public static int openFileDescriptor(String uri, String mode) throws Exception { + if (mSingleton == null) { + return -1; + } try { - android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); - } catch (Exception e) { - Log.v("SDL", "modify thread properties failed " + e.toString()); + ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); + return pfd != null ? pfd.detachFd() : -1; + } catch (FileNotFoundException e) { + e.printStackTrace(); + return -1; } - - Log.v("SDL", "Running main function " + function + " from library " + library); - - SDLActivity.nativeRunMain(library, function, arguments); - - Log.v("SDL", "Finished main function"); - - if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { - // Let's finish the Activity - SDLActivity.mSDLThread = null; - SDLActivity.mSingleton.finish(); - } // else: Activity is already being destroyed - - } -} - -/* This is a fake invisible editor view that receives the input and defines the - * pan&scan region - */ -class DummyEdit extends View implements View.OnKeyListener { - InputConnection ic; - - public DummyEdit(Context context) { - super(context); - setFocusableInTouchMode(true); - setFocusable(true); - setOnKeyListener(this); } - @Override - public boolean onCheckIsTextEditor() { - return true; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - return SDLActivity.handleKeyEvent(v, keyCode, event, ic); - } - - // - @Override - public boolean onKeyPreIme (int keyCode, KeyEvent event) { - // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event - // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639 - // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not - // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout - // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android - // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :) - if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { - if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) { - SDLActivity.onNativeKeyboardFocusLost(); - } + /** + * This method is called by SDL using JNI. + */ + public static boolean showFileDialog(String[] filters, boolean allowMultiple, boolean forWrite, int requestCode) { + if (mSingleton == null) { + return false; } - return super.onKeyPreIme(keyCode, event); - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - ic = new SDLInputConnection(this, true); - - 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; - } -} - -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 - public boolean sendKeyEvent(KeyEvent event) { - /* - * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) - * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses - * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys - * that still do, we empty this out. - */ + if (forWrite) { + allowMultiple = false; + } - /* - * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key - * as we do with physical keyboards, let's just use it to hide the keyboard. - */ + /* Convert string list of extensions to their respective MIME types */ + ArrayList mimes = new ArrayList<>(); + MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); + if (filters != null) { + for (String pattern : filters) { + String[] extensions = pattern.split(";"); - if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { - if (SDLActivity.onNativeSoftReturnKey()) { - return true; + if (extensions.length == 1 && extensions[0].equals("*")) { + /* Handle "*" special case */ + mimes.add("*/*"); + } else { + for (String ext : extensions) { + String mime = mimeTypeMap.getMimeTypeFromExtension(ext); + if (mime != null) { + mimes.add(mime); + } + } + } } } - return super.sendKeyEvent(event); - } + /* Display the file dialog */ + Intent intent = new Intent(forWrite ? Intent.ACTION_CREATE_DOCUMENT : Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + switch (mimes.size()) { + case 0: + intent.setType("*/*"); + break; + case 1: + intent.setType(mimes.get(0)); + break; + default: + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.toArray(new String[]{})); + } - @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - if (!super.commitText(text, newCursorPosition)) { + try { + mSingleton.startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Unable to open file dialog.", e); return false; } - updateText(); + + /* Save current dialog state */ + mFileDialogState = new SDLFileDialogState(); + mFileDialogState.requestCode = requestCode; + mFileDialogState.multipleChoice = allowMultiple; return true; } - @Override - public boolean setComposingText(CharSequence text, int newCursorPosition) { - if (!super.setComposingText(text, newCursorPosition)) { - return false; - } - updateText(); - return true; + /* Internal class used to track active open file dialog */ + static class SDLFileDialogState { + int requestCode; + boolean multipleChoice; } +} +/** + Simple runnable to start the SDL application +*/ +class SDLMain implements Runnable { @Override - public boolean deleteSurroundingText(int beforeLength, int afterLength) { - if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { - // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection - // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 - if (beforeLength > 0 && afterLength == 0) { - // backspace(s) - while (beforeLength-- > 0) { - nativeGenerateScancodeForUnichar('\b'); - } - return true; - } - } + public void run() { + // Runs SDLActivity.main() - if (!super.deleteSurroundingText(beforeLength, afterLength)) { - return false; + try { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY); + } catch (Exception e) { + Log.v("SDL", "modify thread properties failed " + e.toString()); } - updateText(); - return true; - } - protected void updateText() { - final Editable content = getEditable(); - if (content == null) { - return; - } + SDLActivity.nativeInitMainThread(); + SDLActivity.mSingleton.main(); + SDLActivity.nativeCleanupMainThread(); - String text = content.toString(); - int compareLength = Math.min(text.length(), mCommittedText.length()); - int matchLength, offset; + if (SDLActivity.mSingleton != null && !SDLActivity.mSingleton.isFinishing()) { + // Let's finish the Activity + SDLActivity.mSDLThread = null; + SDLActivity.mSDLMainFinished = true; + SDLActivity.mSingleton.finish(); + } // else: Activity is already being destroyed - /* Backspace over characters that are no longer in the string */ - for (matchLength = 0; matchLength < compareLength; ) { - int codePoint = mCommittedText.codePointAt(matchLength); - if (codePoint != text.codePointAt(matchLength)) { - break; - } - matchLength += Character.charCount(codePoint); - } - /* FIXME: This doesn't handle graphemes, like '🌬️' */ - for (offset = matchLength; offset < mCommittedText.length(); ) { - int codePoint = mCommittedText.codePointAt(offset); - nativeGenerateScancodeForUnichar('\b'); - offset += Character.charCount(codePoint); - } - - if (matchLength < text.length()) { - String pendingText = text.subSequence(matchLength, text.length()).toString(); - for (offset = 0; offset < pendingText.length(); ) { - int codePoint = pendingText.codePointAt(offset); - if (codePoint == '\n') { - if (SDLActivity.onNativeSoftReturnKey()) { - return; - } - } - /* Higher code points don't generate simulated scancodes */ - if (codePoint < 128) { - nativeGenerateScancodeForUnichar((char)codePoint); - } - offset += Character.charCount(codePoint); - } - SDLInputConnection.nativeCommitText(pendingText, 0); - } - mCommittedText = text; } - - public static native void nativeCommitText(String text, int newCursorPosition); - - public static native void nativeGenerateScancodeForUnichar(char c); } class SDLClipboardHandler implements diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java b/builds/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java index 7c821a4097..6ad2f543bb 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java +++ b/builds/android/app/src/main/java/org/libsdl/app/SDLAudioManager.java @@ -3,30 +3,21 @@ import android.content.Context; import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; -import android.media.AudioFormat; import android.media.AudioManager; -import android.media.AudioRecord; -import android.media.AudioTrack; -import android.media.MediaRecorder; import android.os.Build; import android.util.Log; import java.util.Arrays; +import java.util.ArrayList; public class SDLAudioManager { 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) */) @@ -34,12 +25,16 @@ public static void initialize() { mAudioDeviceCallback = new AudioDeviceCallback() { @Override public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { - Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId())); + for (AudioDeviceInfo deviceInfo : addedDevices) { + addAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId()); + } } @Override public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { - Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId())); + for (AudioDeviceInfo deviceInfo : removedDevices) { + removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()); + } } }; } @@ -47,451 +42,68 @@ public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { public static void setContext(Context context) { mContext = context; - if (context != null) { - registerAudioDeviceCallback(); - } } public static void release(Context context) { - unregisterAudioDeviceCallback(context); + // no-op atm } // Audio - protected static String getAudioFormatString(int audioFormat) { - switch (audioFormat) { - case AudioFormat.ENCODING_PCM_8BIT: - return "8-bit"; - case AudioFormat.ENCODING_PCM_16BIT: - return "16-bit"; - case AudioFormat.ENCODING_PCM_FLOAT: - return "float"; - default: - return Integer.toString(audioFormat); - } - } - - protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { - int channelConfig; - int sampleSize; - int frameSize; - - Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz"); - - /* On older devices let's use known good settings */ - if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) { - if (desiredChannels > 2) { - desiredChannels = 2; - } - } - - /* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */ - if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) { - if (sampleRate < 8000) { - sampleRate = 8000; - } else if (sampleRate > 48000) { - sampleRate = 48000; - } - } - - if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { - int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */); - if (Build.VERSION.SDK_INT < minSDKVersion) { - audioFormat = AudioFormat.ENCODING_PCM_16BIT; - } - } - switch (audioFormat) - { - case AudioFormat.ENCODING_PCM_8BIT: - sampleSize = 1; - break; - case AudioFormat.ENCODING_PCM_16BIT: - sampleSize = 2; - break; - case AudioFormat.ENCODING_PCM_FLOAT: - sampleSize = 4; - break; - default: - Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT"); - audioFormat = AudioFormat.ENCODING_PCM_16BIT; - sampleSize = 2; - break; - } - - if (isCapture) { - switch (desiredChannels) { - case 1: - channelConfig = AudioFormat.CHANNEL_IN_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_IN_STEREO; - break; - default: - Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); - desiredChannels = 2; - channelConfig = AudioFormat.CHANNEL_IN_STEREO; - break; - } - } else { - switch (desiredChannels) { - case 1: - channelConfig = AudioFormat.CHANNEL_OUT_MONO; - break; - case 2: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - case 3: - channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 4: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD; - break; - case 5: - channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; - break; - case 6: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - break; - case 7: - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; - break; - case 8: - if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) { - channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; - } else { - Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); - desiredChannels = 6; - channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; - } - break; - default: - Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); - desiredChannels = 2; - channelConfig = AudioFormat.CHANNEL_OUT_STEREO; - break; - } - -/* - Log.v(TAG, "Speaker configuration (and order of channels):"); - - if ((channelConfig & 0x00000004) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT"); - } - if ((channelConfig & 0x00000008) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT"); - } - if ((channelConfig & 0x00000010) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER"); - } - if ((channelConfig & 0x00000020) != 0) { - Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY"); - } - if ((channelConfig & 0x00000040) != 0) { - Log.v(TAG, " CHANNEL_OUT_BACK_LEFT"); - } - if ((channelConfig & 0x00000080) != 0) { - Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT"); - } - if ((channelConfig & 0x00000100) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER"); - } - if ((channelConfig & 0x00000200) != 0) { - Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER"); - } - if ((channelConfig & 0x00000400) != 0) { - Log.v(TAG, " CHANNEL_OUT_BACK_CENTER"); - } - if ((channelConfig & 0x00000800) != 0) { - Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT"); - } - if ((channelConfig & 0x00001000) != 0) { - Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT"); - } -*/ - } - frameSize = (sampleSize * desiredChannels); - - // Let the user pick a larger buffer if they really want -- but ye - // gods they probably shouldn't, the minimums are horrifyingly high - // latency already - int minBufferSize; - if (isCapture) { - minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); - } else { - minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); - } - desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize); - - int[] results = new int[4]; - - if (isCapture) { - if (mAudioRecord == null) { - mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, - channelConfig, audioFormat, desiredFrames * frameSize); - - // see notes about AudioTrack state in audioOpen(), above. Probably also applies here. - if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { - Log.e(TAG, "Failed during initialization of AudioRecord"); - mAudioRecord.release(); - mAudioRecord = null; - return null; - } - - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { - mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId)); - } - - mAudioRecord.startRecording(); - } - - results[0] = mAudioRecord.getSampleRate(); - results[1] = mAudioRecord.getAudioFormat(); - results[2] = mAudioRecord.getChannelCount(); - - } else { - if (mAudioTrack == null) { - mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); - - // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid - // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java - // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() - if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { - /* Try again, with safer values */ - - Log.e(TAG, "Failed during initialization of Audio Track"); - mAudioTrack.release(); - mAudioTrack = null; - return null; - } - - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { - mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId)); - } - - mAudioTrack.play(); - } - - results[0] = mAudioTrack.getSampleRate(); - results[1] = mAudioTrack.getAudioFormat(); - results[2] = mAudioTrack.getChannelCount(); - } - results[3] = desiredFrames; - - Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); - - return results; - } - private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) { if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) - .filter(deviceInfo -> deviceInfo.getId() == deviceId) - .findFirst() - .orElse(null); - } else { - return null; - } - } - - private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) - .filter(deviceInfo -> deviceInfo.getId() == deviceId) - .findFirst() - .orElse(null); - } else { - return null; - } - } - - private static void registerAudioDeviceCallback() { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null); - } - } - - private static void unregisterAudioDeviceCallback(Context context) { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback); - } - } - - /** - * This method is called by SDL using JNI. - */ - public static int[] getAudioOutputDevices() { - if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray(); - } else { - return NO_DEVICES; + for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) { + if (deviceInfo.getId() == deviceId) { + return deviceInfo; + } + } } + return null; } - /** - * This method is called by SDL using JNI. - */ - public static int[] getAudioInputDevices() { + private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) { if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); - return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray(); - } else { - return NO_DEVICES; - } - } - - /** - * This method is called by SDL using JNI. - */ - public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { - return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId); - } - - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteFloatBuffer(float[] buffer) { - if (mAudioTrack == null) { - Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); - return; - } - - if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) { - Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)"); - return; - } - - for (int i = 0; i < buffer.length;) { - int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom + for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (deviceInfo.getId() == deviceId) { + return deviceInfo; } - } else { - Log.w(TAG, "SDL audio: error return from write(float)"); - return; } } + return null; } - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteShortBuffer(short[] buffer) { - if (mAudioTrack == null) { - Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); - return; - } - - for (int i = 0; i < buffer.length;) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom + public static void registerAudioDeviceCallback() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + // get an initial list now, before hotplug callbacks fire. + for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) { + if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) { + continue; // Device cannot be opened } - } else { - Log.w(TAG, "SDL audio: error return from write(short)"); - return; + addAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId()); } - } - } - - /** - * This method is called by SDL using JNI. - */ - public static void audioWriteByteBuffer(byte[] buffer) { - if (mAudioTrack == null) { - Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); - return; - } - - for (int i = 0; i < buffer.length; ) { - int result = mAudioTrack.write(buffer, i, buffer.length - i); - if (result > 0) { - i += result; - } else if (result == 0) { - try { - Thread.sleep(1); - } catch(InterruptedException e) { - // Nom nom - } - } else { - Log.w(TAG, "SDL audio: error return from write(byte)"); - return; + for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) { + addAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId()); } + audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null); } } - /** - * This method is called by SDL using JNI. - */ - public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) { - return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId); - } - - /** This method is called by SDL using JNI. */ - public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { - return 0; - } else { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); - } - } - - /** This method is called by SDL using JNI. */ - public static int captureReadShortBuffer(short[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { - return mAudioRecord.read(buffer, 0, buffer.length); - } else { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); - } - } - - /** This method is called by SDL using JNI. */ - public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { - if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) { - return mAudioRecord.read(buffer, 0, buffer.length); - } else { - return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); - } - } - - /** This method is called by SDL using JNI. */ - public static void audioClose() { - if (mAudioTrack != null) { - mAudioTrack.stop(); - mAudioTrack.release(); - mAudioTrack = null; - } - } - - /** This method is called by SDL using JNI. */ - public static void captureClose() { - if (mAudioRecord != null) { - mAudioRecord.stop(); - mAudioRecord.release(); - mAudioRecord = null; + public static void unregisterAudioDeviceCallback() { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { + AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback); } } /** This method is called by SDL using JNI. */ - public static void audioSetThreadPriority(boolean iscapture, int device_id) { + public static void audioSetThreadPriority(boolean recording, int device_id) { try { /* Set thread name */ - if (iscapture) { + if (recording) { Thread.currentThread().setName("SDLAudioC" + device_id); } else { Thread.currentThread().setName("SDLAudioP" + device_id); @@ -507,8 +119,8 @@ 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 removeAudioDevice(boolean recording, int deviceId); - public static native void addAudioDevice(boolean isCapture, int deviceId); + public static native void addAudioDevice(boolean recording, String name, int deviceId); } diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/builds/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java index 9d8b20b7bb..b7faee8997 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/builds/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -9,6 +9,7 @@ import android.os.Build; import android.os.VibrationEffect; import android.os.Vibrator; +import android.os.VibratorManager; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; @@ -21,15 +22,15 @@ public class SDLControllerManager public static native int nativeSetupJNI(); - public static native int nativeAddJoystick(int device_id, String name, String desc, - int vendor_id, int product_id, - boolean is_accelerometer, int button_mask, - int naxes, int axis_mask, int nhats, int nballs); - public static native int nativeRemoveJoystick(int device_id); - public static native int nativeAddHaptic(int device_id, String name); - public static native int nativeRemoveHaptic(int device_id); - public static native int onNativePadDown(int device_id, int keycode); - public static native int onNativePadUp(int device_id, int keycode); + public static native void nativeAddJoystick(int device_id, String name, String desc, + int vendor_id, int product_id, + int button_mask, + int naxes, int axis_mask, int nhats, boolean can_rumble); + public static native void nativeRemoveJoystick(int device_id); + public static native void nativeAddHaptic(int device_id, String name); + public static native void nativeRemoveHaptic(int device_id); + public static native boolean onNativePadDown(int device_id, int keycode); + public static native boolean onNativePadUp(int device_id, int keycode); public static native void onNativeJoy(int device_id, int axis, float value); public static native void onNativeHat(int device_id, int hat_id, @@ -50,7 +51,9 @@ public static void initialize() { } if (mHapticHandler == null) { - if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + mHapticHandler = new SDLHapticHandler_API31(); + } else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { mHapticHandler = new SDLHapticHandler_API26(); } else { mHapticHandler = new SDLHapticHandler(); @@ -84,6 +87,13 @@ public static void hapticRun(int device_id, float intensity, int length) { mHapticHandler.run(device_id, intensity, length); } + /** + * This method is called by SDL using JNI. + */ + public static void hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { + mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_intensity, length); + } + /** * This method is called by SDL using JNI. */ @@ -233,10 +243,19 @@ public void pollInputDevices() { } } + boolean can_rumble = false; + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { + VibratorManager manager = joystickDevice.getVibratorManager(); + int[] vibrators = manager.getVibratorIds(); + if (vibrators.length > 0) { + can_rumble = true; + } + } + mJoysticks.add(joystick); SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, - getVendorId(joystickDevice), getProductId(joystickDevice), false, - getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0); + getVendorId(joystickDevice), getProductId(joystickDevice), + getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble); } } } @@ -470,12 +489,63 @@ public int getButtonMask(InputDevice joystickDevice) { } } +class SDLHapticHandler_API31 extends SDLHapticHandler { + @Override + public void run(int device_id, float intensity, int length) { + SDLHaptic haptic = getHaptic(device_id); + if (haptic != null) { + vibrate(haptic.vib, intensity, length); + } + } + + @Override + public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { + InputDevice device = InputDevice.getDevice(device_id); + if (device == null) { + return; + } + + VibratorManager manager = device.getVibratorManager(); + int[] vibrators = manager.getVibratorIds(); + if (vibrators.length >= 2) { + vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length); + vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length); + } else if (vibrators.length == 1) { + float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f); + vibrate(manager.getVibrator(vibrators[0]), intensity, length); + } + } + + private void vibrate(Vibrator vibrator, float intensity, int length) { + if (intensity == 0.0f) { + vibrator.cancel(); + return; + } + + int value = Math.round(intensity * 255); + if (value > 255) { + value = 255; + } + if (value < 1) { + vibrator.cancel(); + return; + } + try { + vibrator.vibrate(VibrationEffect.createOneShot(length, value)); + } + catch (Exception e) { + // Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if + // something went horribly wrong with the Android 8.0 APIs. + vibrator.vibrate(length); + } + } +} + class SDLHapticHandler_API26 extends SDLHapticHandler { @Override public void run(int device_id, float intensity, int length) { SDLHaptic haptic = getHaptic(device_id); if (haptic != null) { - Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length); if (intensity == 0.0f) { stop(device_id); return; @@ -523,6 +593,10 @@ public void run(int device_id, float intensity, int length) { } } + public void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) { + // Not supported in older APIs + } + public void stop(int device_id) { SDLHaptic haptic = getHaptic(device_id); if (haptic != null) { @@ -535,30 +609,6 @@ public void pollHapticDevices() { final int deviceId_VIBRATOR_SERVICE = 999999; boolean hasVibratorService = false; - int[] deviceIds = InputDevice.getDeviceIds(); - // It helps processing the device ids in reverse order - // For example, in the case of the XBox 360 wireless dongle, - // so the first controller seen by SDL matches what the receiver - // considers to be the first controller - - for (int i = deviceIds.length - 1; i > -1; i--) { - SDLHaptic haptic = getHaptic(deviceIds[i]); - if (haptic == null) { - InputDevice device = InputDevice.getDevice(deviceIds[i]); - Vibrator vib = device.getVibrator(); - if (vib != null) { - if (vib.hasVibrator()) { - haptic = new SDLHaptic(); - haptic.device_id = deviceIds[i]; - haptic.name = device.getName(); - haptic.vib = vib; - mHaptics.add(haptic); - SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name); - } - } - } - } - /* Check VIBRATOR_SERVICE */ Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE); if (vib != null) { @@ -581,18 +631,11 @@ public void pollHapticDevices() { ArrayList removedDevices = null; for (SDLHaptic haptic : mHaptics) { int device_id = haptic.device_id; - int i; - for (i = 0; i < deviceIds.length; i++) { - if (device_id == deviceIds[i]) break; - } - if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) { - if (i == deviceIds.length) { - if (removedDevices == null) { - removedDevices = new ArrayList(); - } - removedDevices.add(device_id); + if (removedDevices == null) { + removedDevices = new ArrayList(); } + removedDevices.add(device_id); } // else: don't remove the vibrator if it is still present } diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLDummyEdit.java b/builds/android/app/src/main/java/org/libsdl/app/SDLDummyEdit.java new file mode 100644 index 0000000000..40e556ff41 --- /dev/null +++ b/builds/android/app/src/main/java/org/libsdl/app/SDLDummyEdit.java @@ -0,0 +1,66 @@ +package org.libsdl.app; + +import android.content.*; +import android.text.InputType; +import android.view.*; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/* This is a fake invisible editor view that receives the input and defines the + * pan&scan region + */ +public class SDLDummyEdit extends View implements View.OnKeyListener +{ + InputConnection ic; + int input_type; + + public SDLDummyEdit(Context context) { + super(context); + setFocusableInTouchMode(true); + setFocusable(true); + setOnKeyListener(this); + } + + public void setInputType(int input_type) { + this.input_type = input_type; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return SDLActivity.handleKeyEvent(v, keyCode, event, ic); + } + + // + @Override + public boolean onKeyPreIme (int keyCode, KeyEvent event) { + // As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event + // FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639 + // FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not + // FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout + // FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android + // FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :) + if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) { + SDLActivity.onNativeKeyboardFocusLost(); + } + } + return super.onKeyPreIme(keyCode, event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + ic = new SDLInputConnection(this, true); + + outAttrs.inputType = input_type; + outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | + EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */; + + return ic; + } +} + diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/builds/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java new file mode 100644 index 0000000000..e1d29a8890 --- /dev/null +++ b/builds/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java @@ -0,0 +1,136 @@ +package org.libsdl.app; + +import android.content.*; +import android.os.Build; +import android.text.Editable; +import android.view.*; +import android.view.inputmethod.BaseInputConnection; +import android.widget.EditText; + +public 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 + public boolean sendKeyEvent(KeyEvent event) { + /* + * This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard) + * However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses + * and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys + * that still do, we empty this out. + */ + + /* + * Return DOES still generate a key event, however. So rather than using it as the 'click a button' key + * as we do with physical keyboards, let's just use it to hide the keyboard. + */ + + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { + if (SDLActivity.onNativeSoftReturnKey()) { + return true; + } + } + + return super.sendKeyEvent(event); + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + if (!super.commitText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (!super.setComposingText(text, newCursorPosition)) { + return false; + } + updateText(); + return true; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + 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; + } + } + + if (!super.deleteSurroundingText(beforeLength, afterLength)) { + return false; + } + updateText(); + return true; + } + + protected void updateText() { + final Editable content = getEditable(); + if (content == null) { + return; + } + + String text = content.toString(); + int compareLength = Math.min(text.length(), mCommittedText.length()); + int matchLength, offset; + + /* Backspace over characters that are no longer in the string */ + for (matchLength = 0; matchLength < compareLength; ) { + int codePoint = mCommittedText.codePointAt(matchLength); + if (codePoint != text.codePointAt(matchLength)) { + break; + } + matchLength += Character.charCount(codePoint); + } + /* FIXME: This doesn't handle graphemes, like '🌬️' */ + for (offset = matchLength; offset < mCommittedText.length(); ) { + int codePoint = mCommittedText.codePointAt(offset); + nativeGenerateScancodeForUnichar('\b'); + offset += Character.charCount(codePoint); + } + + if (matchLength < text.length()) { + String pendingText = text.subSequence(matchLength, text.length()).toString(); + for (offset = 0; offset < pendingText.length(); ) { + int codePoint = pendingText.codePointAt(offset); + if (codePoint == '\n') { + if (SDLActivity.onNativeSoftReturnKey()) { + return; + } + } + /* Higher code points don't generate simulated scancodes */ + if (codePoint < 128) { + nativeGenerateScancodeForUnichar((char)codePoint); + } + offset += Character.charCount(codePoint); + } + SDLInputConnection.nativeCommitText(pendingText, 0); + } + mCommittedText = text; + } + + public static native void nativeCommitText(String text, int newCursorPosition); + + public static native void nativeGenerateScancodeForUnichar(char c); +} + diff --git a/builds/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/builds/android/app/src/main/java/org/libsdl/app/SDLSurface.java index 0857e4b6f3..d896f6ce4a 100644 --- a/builds/android/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/builds/android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -3,6 +3,7 @@ import android.content.Context; import android.content.pm.ActivityInfo; +import android.graphics.Insets; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; @@ -18,6 +19,7 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; +import android.view.WindowInsets; import android.view.WindowManager; @@ -28,7 +30,7 @@ Because of this, that's where we set up the SDL thread */ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, - View.OnKeyListener, View.OnTouchListener, SensorEventListener { + View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, SensorEventListener { // Sensors protected SensorManager mSensorManager; @@ -48,6 +50,7 @@ public SDLSurface(Context context) { setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + setOnApplyWindowInsetsListener(this); setOnKeyListener(this); setOnTouchListener(this); @@ -71,6 +74,7 @@ public void handleResume() { setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + setOnApplyWindowInsetsListener(this); setOnKeyListener(this); setOnTouchListener(this); enableSensor(Sensor.TYPE_ACCELEROMETER, true); @@ -114,6 +118,7 @@ public void surfaceChanged(SurfaceHolder holder, mHeight = height; int nDeviceWidth = width; int nDeviceHeight = height; + float density = 1.0f; try { if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) { @@ -121,6 +126,8 @@ public void surfaceChanged(SurfaceHolder holder, mDisplay.getRealMetrics( realMetrics ); nDeviceWidth = realMetrics.widthPixels; nDeviceHeight = realMetrics.heightPixels; + // Use densityDpi instead of density to more closely match what the UI scale is + density = (float)realMetrics.densityDpi / 160.0f; } } catch(Exception ignored) { } @@ -132,7 +139,7 @@ public void surfaceChanged(SurfaceHolder holder, Log.v("SDL", "Window size: " + width + "x" + height); Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight); - SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate()); + SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate()); SDLActivity.onNativeResize(); // Prevent a screen distortion glitch, @@ -161,13 +168,10 @@ public void surfaceChanged(SurfaceHolder holder, } } - // Don't skip in MultiWindow. + // Don't skip if we might be multi-window or have popup dialogs if (skip) { if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { - if (SDLActivity.mSingleton.isInMultiWindowMode()) { - Log.v("SDL", "Don't skip in Multi-Window"); - skip = false; - } + skip = false; } } @@ -187,12 +191,47 @@ public void surfaceChanged(SurfaceHolder holder, SDLActivity.handleNativeState(); } + // Window inset + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) { + Insets combined = insets.getInsets(WindowInsets.Type.systemBars() | + WindowInsets.Type.systemGestures() | + WindowInsets.Type.mandatorySystemGestures() | + WindowInsets.Type.tappableElement() | + WindowInsets.Type.displayCutout()); + + SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom); + } + + // Pass these to any child views in case they need them + return insets; + } + // Key events @Override public boolean onKey(View v, int keyCode, KeyEvent event) { return SDLActivity.handleKeyEvent(v, keyCode, event, null); } + private float getNormalizedX(float x) + { + if (mWidth <= 1) { + return 0.5f; + } else { + return (x / (mWidth - 1)); + } + } + + private float getNormalizedY(float y) + { + if (mHeight <= 1) { + return 0.5f; + } else { + return (y / (mHeight - 1)); + } + } + // Touch events @Override public boolean onTouch(View v, MotionEvent event) { @@ -204,16 +243,6 @@ public boolean onTouch(View v, MotionEvent event) { 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 @@ -239,8 +268,8 @@ public boolean onTouch(View v, MotionEvent event) { case MotionEvent.ACTION_MOVE: for (i = 0; i < pointerCount; i++) { pointerFingerId = event.getPointerId(i); - x = event.getX(i) / mWidth; - y = event.getY(i) / mHeight; + x = getNormalizedX(event.getX(i)); + y = getNormalizedY(event.getY(i)); p = event.getPressure(i); if (p > 1.0f) { // may be larger than 1.0f on some devices @@ -264,8 +293,8 @@ public boolean onTouch(View v, MotionEvent event) { } pointerFingerId = event.getPointerId(i); - x = event.getX(i) / mWidth; - y = event.getY(i) / mHeight; + x = getNormalizedX(event.getX(i)); + y = getNormalizedY(event.getY(i)); p = event.getPressure(i); if (p > 1.0f) { // may be larger than 1.0f on some devices @@ -278,8 +307,8 @@ public boolean onTouch(View v, MotionEvent event) { case MotionEvent.ACTION_CANCEL: for (i = 0; i < pointerCount; i++) { pointerFingerId = event.getPointerId(i); - x = event.getX(i) / mWidth; - y = event.getY(i) / mHeight; + x = getNormalizedX(event.getX(i)); + y = getNormalizedY(event.getY(i)); p = event.getPressure(i); if (p > 1.0f) { // may be larger than 1.0f on some devices @@ -322,36 +351,36 @@ public void onSensorChanged(SensorEvent event) { // Since we may have an orientation set, we won't receive onConfigurationChanged events. // We thus should check here. - int newOrientation; + int newRotation; float x, y; switch (mDisplay.getRotation()) { + case Surface.ROTATION_0: + default: + x = event.values[0]; + y = event.values[1]; + newRotation = 0; + break; case Surface.ROTATION_90: x = -event.values[1]; y = event.values[0]; - newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE; - break; - case Surface.ROTATION_270: - x = event.values[1]; - y = -event.values[0]; - newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED; + newRotation = 90; break; case Surface.ROTATION_180: x = -event.values[0]; y = -event.values[1]; - newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED; + newRotation = 180; break; - case Surface.ROTATION_0: - default: - x = event.values[0]; - y = event.values[1]; - newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT; + case Surface.ROTATION_270: + x = event.values[1]; + y = -event.values[0]; + newRotation = 270; break; } - if (newOrientation != SDLActivity.mCurrentOrientation) { - SDLActivity.mCurrentOrientation = newOrientation; - SDLActivity.onNativeOrientationChanged(newOrientation); + if (newRotation != SDLActivity.mCurrentRotation) { + SDLActivity.mCurrentRotation = newRotation; + SDLActivity.onNativeRotationChanged(newRotation); } SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,