From 4e2d7463cda7630d76c8b26c6dc63b2b73f0e3e5 Mon Sep 17 00:00:00 2001 From: Maksym H Date: Sat, 8 Jul 2023 18:19:24 +0300 Subject: [PATCH] Android: minor update --- .../app/HIDDeviceBLESteamController.java | 4 +- .../java/org/libsdl/app/HIDDeviceManager.java | 16 +- .../java/org/libsdl/app/HIDDeviceUSB.java | 6 +- .../app/src/main/java/org/libsdl/app/SDL.java | 1 + .../main/java/org/libsdl/app/SDLActivity.java | 67 ++++--- .../java/org/libsdl/app/SDLAudioManager.java | 164 +++++++++++++++--- .../org/libsdl/app/SDLControllerManager.java | 52 +++++- .../main/java/org/libsdl/app/SDLSurface.java | 4 +- Android/native/build.gradle | 2 +- Android/native/jni/Application.mk | 4 +- src/porting_android.cpp | 55 +++--- 11 files changed, 274 insertions(+), 101 deletions(-) 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 65c5a4237..ee5521fd5 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 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe // 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 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe } }); } - } + } else if (newState == 0) { mIsConnected = false; } 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 cf3c9267f..6f7013b2f 100644 --- a/Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/Android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -170,7 +170,7 @@ public class HIDDeviceManager { 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); @@ -251,6 +251,8 @@ public class HIDDeviceManager { 0x20d6, // PowerA 0x24c6, // PowerA 0x2c22, // Qanba + 0x2dc8, // 8BitDo + 0x9886, // ASTRO Gaming }; if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC && @@ -271,14 +273,16 @@ public class HIDDeviceManager { final int XB1_IFACE_SUBCLASS = 71; final int XB1_IFACE_PROTOCOL = 208; final int[] SUPPORTED_VENDORS = { + 0x044f, // Thrustmaster 0x045e, // Microsoft 0x0738, // Mad Catz 0x0e6f, // PDP 0x0f0d, // Hori + 0x10f5, // Turtle Beach 0x1532, // Razer Wildcat 0x20d6, // PowerA 0x24c6, // PowerA - 0x2dc8, /* 8BitDo */ + 0x2dc8, // 8BitDo 0x2e24, // Hyperkin }; @@ -353,13 +357,13 @@ public class HIDDeviceManager { private void initializeBluetooth() { Log.d(TAG, "Initializing Bluetooth"); - if (Build.VERSION.SDK_INT <= 30 && + 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)) { + 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; } @@ -524,7 +528,7 @@ public class HIDDeviceManager { for (HIDDevice device : mDevicesById.values()) { device.setFrozen(frozen); } - } + } } ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -573,7 +577,7 @@ public class HIDDeviceManager { try { final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31 int flags; - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { flags = FLAG_MUTABLE; } else { flags = 0; 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 d20fe80bc..bfe0cf954 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 @@ class HIDDeviceUSB implements HIDDevice { @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 @@ class HIDDeviceUSB implements HIDDevice { @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 @@ class HIDDeviceUSB implements HIDDevice { @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 dafc0cb87..44c21c1c7 100644 --- a/Android/app/src/main/java/org/libsdl/app/SDL.java +++ b/Android/app/src/main/java/org/libsdl/app/SDL.java @@ -29,6 +29,7 @@ public class SDL { // This function stores the current activity (SDL or not) public static void setContext(Context context) { + SDLAudioManager.setContext(context); mContext = context; } 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 9ed0e7ce7..ed0d4d037 100644 --- a/Android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/Android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -60,8 +60,8 @@ import java.util.Locale; 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 = 26; - private static final int SDL_MICRO_VERSION = 5; + private static final int SDL_MINOR_VERSION = 28; + private static final int SDL_MICRO_VERSION = 1; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -93,7 +93,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= 23) { tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; s2 &= ~tst; @@ -107,7 +107,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if ((s & tst) == tst) src += " GAMEPAD"; s2 &= ~tst; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= 21) { tst = InputDevice.SOURCE_HDMI; if ((s & tst) == tst) src += " HDMI"; s2 &= ~tst; @@ -146,7 +146,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if ((s & tst) == tst) src += " TOUCHSCREEN"; s2 &= ~tst; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (Build.VERSION.SDK_INT >= 18) { tst = InputDevice.SOURCE_TOUCH_NAVIGATION; if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; s2 &= ~tst; @@ -170,7 +170,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh */ 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) */); // Cursor types // private static final int SDL_SYSTEM_CURSOR_NONE = -1; @@ -224,9 +224,9 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh 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(); @@ -393,7 +393,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh mHIDDeviceManager = HIDDeviceManager.acquire(this); // Set up the surface - mSurface = createSDLSurface(getApplication()); + mSurface = createSDLSurface(this); mLayout = new RelativeLayout(this); mLayout.addView(mSurface); @@ -404,7 +404,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh SDLActivity.onNativeOrientationChanged(mCurrentOrientation); try { - if (Build.VERSION.SDK_INT < 24) { + if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { mCurrentLocale = getContext().getResources().getConfiguration().locale; } else { mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); @@ -588,6 +588,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh mHIDDeviceManager = null; } + SDLAudioManager.release(this); + if (SDLActivity.mBrokenLibraries) { super.onDestroy(); return; @@ -766,7 +768,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh } break; case COMMAND_CHANGE_WINDOW_STYLE: - if (Build.VERSION.SDK_INT >= 19) { + if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { if (context instanceof Activity) { Window window = ((Activity) context).getWindow(); if (window != null) { @@ -841,7 +843,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh msg.obj = data; boolean result = commandHandler.sendMessage(msg); - if (Build.VERSION.SDK_INT >= 19) { + 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. @@ -969,15 +971,18 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh /* 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; @@ -1090,7 +1095,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh // 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; } @@ -1180,7 +1185,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh * 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 { @@ -1617,7 +1622,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh private final Runnable rehideSystemUi = new Runnable() { @Override public void run() { - if (Build.VERSION.SDK_INT >= 19) { + 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 | @@ -1670,7 +1675,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh 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) { @@ -1686,7 +1691,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh * This method is called by SDL using JNI. */ public static void destroyCustomCursor(int cursorID) { - if (Build.VERSION.SDK_INT >= 24) { + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { try { mCursors.remove(cursorID); } catch (Exception e) { @@ -1700,7 +1705,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh */ 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) { @@ -1755,7 +1760,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh 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) { @@ -1769,7 +1774,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh * 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; } @@ -1798,7 +1803,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh i.setData(Uri.parse(url)); int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; - if (Build.VERSION.SDK_INT >= 21) { + 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; @@ -2002,6 +2007,18 @@ class SDLInputConnection extends BaseInputConnection { @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; } 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 2bfc71860..7c821a409 100644 --- a/Android/app/src/main/java/org/libsdl/app/SDLAudioManager.java +++ b/Android/app/src/main/java/org/libsdl/app/SDLAudioManager.java @@ -1,5 +1,8 @@ package org.libsdl.app; +import android.content.Context; +import android.media.AudioDeviceCallback; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; @@ -8,34 +11,67 @@ 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; @@ -43,14 +79,14 @@ public class SDLAudioManager 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) { + if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) { if (sampleRate < 8000) { sampleRate = 8000; } else if (sampleRate > 48000) { @@ -59,7 +95,7 @@ public class SDLAudioManager } 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; } @@ -120,7 +156,7 @@ public class SDLAudioManager 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"); @@ -201,6 +237,10 @@ public class SDLAudioManager return null; } + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { + mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId)); + } + mAudioRecord.startRecording(); } @@ -224,6 +264,10 @@ public class SDLAudioManager return null; } + if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) { + mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId)); + } + mAudioTrack.play(); } @@ -238,11 +282,73 @@ public class SDLAudioManager 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); } /** @@ -254,6 +360,11 @@ public class SDLAudioManager 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) { @@ -326,18 +437,22 @@ public class SDLAudioManager /** * 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); @@ -346,7 +461,7 @@ public class SDLAudioManager /** 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); @@ -391,4 +506,9 @@ public class SDLAudioManager } 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 cc6cc3b59..d6913f157 100644 --- a/Android/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/Android/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -24,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); @@ -42,7 +42,7 @@ public class SDLControllerManager 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(); @@ -50,7 +50,7 @@ public class SDLControllerManager } 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(); @@ -236,7 +236,7 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler { 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); + getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0); } } } @@ -317,6 +317,9 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler { public int getVendorId(InputDevice joystickDevice) { return 0; } + public int getAxisMask(List ranges) { + return -1; + } public int getButtonMask(InputDevice joystickDevice) { return -1; } @@ -334,6 +337,43 @@ class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 { 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; @@ -769,7 +809,7 @@ class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { @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 @@ -779,7 +819,7 @@ class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 { @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 { diff --git a/Android/app/src/main/java/org/libsdl/app/SDLSurface.java b/Android/app/src/main/java/org/libsdl/app/SDLSurface.java index dcd26d495..0857e4b6f 100644 --- a/Android/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/Android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -116,7 +116,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, int nDeviceHeight = height; try { - if (Build.VERSION.SDK_INT >= 17) { + if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) { DisplayMetrics realMetrics = new DisplayMetrics(); mDisplay.getRealMetrics( realMetrics ); nDeviceWidth = realMetrics.widthPixels; @@ -163,7 +163,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, // Don't skip in MultiWindow. if (skip) { - if (Build.VERSION.SDK_INT >= 24) { + 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; diff --git a/Android/native/build.gradle b/Android/native/build.gradle index 6339e0b55..5d00f439f 100644 --- a/Android/native/build.gradle +++ b/Android/native/build.gradle @@ -51,7 +51,7 @@ android { // get precompiled deps tasks.register('downloadDeps', Download) { - def VERSION = "29052023" + def VERSION = "02072023" src "https://github.com/MultiCraft/deps_android/releases/download/$VERSION/deps_android.zip" dest new File(buildDir, 'deps.zip') overwrite false diff --git a/Android/native/jni/Application.mk b/Android/native/jni/Application.mk index b4489807d..73e92af84 100644 --- a/Android/native/jni/Application.mk +++ b/Android/native/jni/Application.mk @@ -5,9 +5,9 @@ APP_SHORT_COMMANDS := true APP_MODULES := MultiCraft ifdef NDEBUG -APP_CFLAGS := -Ofast -fvisibility=hidden -Wno-extra-tokens -D__FILE__=__FILE_NAME__ -Wno-builtin-macro-redefined +APP_CFLAGS := -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -Wno-extra-tokens -D__FILE__=__FILE_NAME__ -Wno-builtin-macro-redefined else -APP_CFLAGS := -g -D_DEBUG -O0 -fno-omit-frame-pointer +APP_CFLAGS := -g -D_DEBUG -O1 -fno-omit-frame-pointer endif APP_CFLAGS += -fexceptions diff --git a/src/porting_android.cpp b/src/porting_android.cpp index 5629a4f7d..4d30bd93f 100644 --- a/src/porting_android.cpp +++ b/src/porting_android.cpp @@ -77,11 +77,6 @@ extern "C" int SDL_main(int argc, char *argv[]) _Exit(0); } -/** - * Handler for finished message box input - * Intentionally NOT in namespace porting - * ToDo: this doesn't work as expected, there's a workaround for it right now - */ extern "C" { JNIEXPORT void JNICALL Java_com_multicraft_game_GameActivity_pauseGame( JNIEnv *env, jclass clazz) @@ -138,7 +133,7 @@ void initAndroid() activityClass = findClass("com/multicraft/game/GameActivity"); if (activityClass == nullptr) errorstream << - "porting::initAndroid unable to find java game activity class" << + "porting::initAndroid unable to find Java game activity class" << std::endl; #ifdef GPROF @@ -158,15 +153,14 @@ void cleanupAndroid() #endif } -static std::string javaStringToUTF8(jstring js) +static std::string readJavaString(jstring j_str) { - std::string str; - // Get string as a UTF-8 c-string - const char *c_str = jnienv->GetStringUTFChars(js, nullptr); + // Get string as a UTF-8 C string + const char *c_str = jnienv->GetStringUTFChars(j_str, nullptr); // Save it - str = c_str; - // And free the c-string - jnienv->ReleaseStringUTFChars(js, c_str); + std::string str(c_str); + // And free the C string + jnienv->ReleaseStringUTFChars(j_str, c_str); return str; } @@ -191,7 +185,7 @@ static std::string getAndroidPath( // Call getAbsolutePath auto js_path = (jstring) jnienv->CallObjectMethod(ob_file, mt_getAbsPath); - return javaStringToUTF8(js_path); + return readJavaString(js_path); } void initializePaths() @@ -221,7 +215,7 @@ void showInputDialog(const std::string &hint, const std::string ¤t, int ed "(Ljava/lang/String;Ljava/lang/String;I)V"); FATAL_ERROR_IF(showdialog == nullptr, - "porting::showInputDialog unable to find java show dialog method"); + "porting::showInputDialog unable to find Java show dialog method"); jstring jhint = jnienv->NewStringUTF(hint.c_str()); jstring jcurrent = jnienv->NewStringUTF(current.c_str()); @@ -236,7 +230,7 @@ void openURIAndroid(const std::string &url) "(Ljava/lang/String;)V"); FATAL_ERROR_IF(url_open == nullptr, - "porting::openURIAndroid unable to find java openURI method"); + "porting::openURIAndroid unable to find Java openURI method"); jstring jurl = jnienv->NewStringUTF(url.c_str()); jnienv->CallVoidMethod(activityObj, url_open, jurl); @@ -248,7 +242,7 @@ int getInputDialogState() "getDialogState", "()I"); FATAL_ERROR_IF(dialogstate == nullptr, - "porting::getInputDialogState unable to find java dialog state method"); + "porting::getInputDialogState unable to find Java dialog state method"); return jnienv->CallIntMethod(activityObj, dialogstate); } @@ -259,15 +253,10 @@ std::string getInputDialogValue() "getDialogValue", "()Ljava/lang/String;"); FATAL_ERROR_IF(dialogvalue == nullptr, - "porting::getInputDialogValue unable to find java dialog value method"); + "porting::getInputDialogValue unable to find Java getDialogValue method"); jobject result = jnienv->CallObjectMethod(activityObj, dialogvalue); - - const char *javachars = jnienv->GetStringUTFChars((jstring) result, nullptr); - std::string text(javachars); - jnienv->ReleaseStringUTFChars((jstring) result, javachars); - - return text; + return readJavaString((jstring) result); } float getTotalSystemMemory() @@ -289,7 +278,7 @@ void handleError(const std::string &errType, const std::string &err) "handleError", "(Ljava/lang/String;)V"); FATAL_ERROR_IF(report_err == nullptr, - "porting::handleError unable to find java handleError method"); + "porting::handleError unable to find Java handleError method"); std::string errorMessage = errType + ": " + err; jstring jerr = porting::getJniString(errorMessage); @@ -302,7 +291,7 @@ void notifyServerConnect(bool is_multiplayer) "notifyServerConnect", "(Z)V"); FATAL_ERROR_IF(notifyConnect == nullptr, - "porting::notifyServerConnect unable to find java notifyServerConnect method"); + "porting::notifyServerConnect unable to find Java notifyServerConnect method"); auto param = (jboolean) is_multiplayer; @@ -318,7 +307,7 @@ void notifyExitGame() "notifyExitGame", "()V"); FATAL_ERROR_IF(notifyExit == nullptr, - "porting::notifyExitGame unable to find java notifyExitGame method"); + "porting::notifyExitGame unable to find Java notifyExitGame method"); jnienv->CallVoidMethod(activityObj, notifyExit); @@ -337,11 +326,12 @@ float getDisplayDensity() "getDensity", "()F"); FATAL_ERROR_IF(getDensity == nullptr, - "porting::getDisplayDensity unable to find java getDensity method"); + "porting::getDisplayDensity unable to find Java getDensity method"); value = jnienv->CallFloatMethod(activityObj, getDensity); firstRun = false; } + return value; } #endif // ndef SERVER @@ -396,7 +386,7 @@ void upgrade(const std::string &item) "upgrade", "(Ljava/lang/String;)V"); FATAL_ERROR_IF(upgradeGame == nullptr, - "porting::upgradeGame unable to find java upgrade method"); + "porting::upgrade unable to find Java upgrade method"); jstring jitem = jnienv->NewStringUTF(item.c_str()); jnienv->CallVoidMethod(activityObj, upgradeGame, jitem); @@ -412,11 +402,12 @@ int getRoundScreen() "getRoundScreen", "()I"); FATAL_ERROR_IF(getRadius == nullptr, - "porting::getRadius unable to find java getRoundScreen method"); + "porting::getRoundScreen unable to find Java getRoundScreen method"); radius = jnienv->CallIntMethod(activityObj, getRadius); firstRun = false; } + return radius; } @@ -426,11 +417,11 @@ std::string getSecretKey(const std::string &key) "getSecretKey", "(Ljava/lang/String;)Ljava/lang/String;"); FATAL_ERROR_IF(getKey == nullptr, - "porting::getSecretKey unable to find java getSecretKey method"); + "porting::getSecretKey unable to find Java getSecretKey method"); jstring jkey = jnienv->NewStringUTF(key.c_str()); auto result = (jstring) jnienv->CallObjectMethod(activityObj, getKey, jkey); - return javaStringToUTF8(result); + return readJavaString(result); } }