diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index d1268e93a5..67c7ad4194 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -59,6 +59,7 @@ import org.dolphinemu.dolphinemu.fragments.EmulationFragment import org.dolphinemu.dolphinemu.fragments.MenuFragment import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment.SaveOrLoad +import org.dolphinemu.dolphinemu.model.HapticEffect import org.dolphinemu.dolphinemu.overlay.InputOverlay import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer import org.dolphinemu.dolphinemu.ui.main.MainPresenter @@ -66,7 +67,6 @@ import org.dolphinemu.dolphinemu.ui.main.ThemeProvider import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner import org.dolphinemu.dolphinemu.utils.DirectoryInitialization import org.dolphinemu.dolphinemu.utils.FileBrowserHelper -import org.dolphinemu.dolphinemu.utils.HapticEffect import org.dolphinemu.dolphinemu.utils.HapticsProvider import org.dolphinemu.dolphinemu.utils.RateLimiter import org.dolphinemu.dolphinemu.utils.ThemeHelper diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/HapticEffect.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/HapticEffect.kt new file mode 100644 index 0000000000..965f3b4340 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/HapticEffect.kt @@ -0,0 +1,95 @@ +package org.dolphinemu.dolphinemu.model + +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.annotation.FloatRange +import androidx.annotation.RequiresApi +import androidx.core.view.HapticFeedbackConstantsCompat + +/** + * Enum to represent haptic effects. + * + * @param feedbackConstant One of the constants defined in [HapticFeedbackConstantsCompat]. + * @param maxTimings The maximum timing values, in milliseconds, of the timing / amplitude pairs. + * Note that lower-end vibrators may not be able to vibrate for short (<50ms) durations. + * @param maxAmplitudes The maximum amplitude values of the timing / amplitude pairs. + * Amplitude values must be between 0 and 255. An amplitude value of 0 implies the motor is off. + * @param primaryPrimitive The primary primitive ID, or null if the SDK version is too low. + * @param fallbackPrimitive The fallback primitive ID, or null if the SDK version is too low. + */ +enum class HapticEffect( + val feedbackConstant: Int, + private val maxTimings: LongArray, + private val maxAmplitudes: IntArray, + val primaryPrimitive: Int?, + val fallbackPrimitive: Int? = null +) { + PRESS( + feedbackConstant = HapticFeedbackConstantsCompat.VIRTUAL_KEY, + maxTimings = longArrayOf(0L, 100L), + maxAmplitudes = intArrayOf(0, 255), + primaryPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + VibrationEffect.Composition.PRIMITIVE_QUICK_FALL + } else { + null + }, + fallbackPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + VibrationEffect.Composition.PRIMITIVE_CLICK + } else { + null + } + ), + RELEASE( + feedbackConstant = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + HapticFeedbackConstantsCompat.VIRTUAL_KEY_RELEASE + } else { + HapticFeedbackConstantsCompat.CONTEXT_CLICK // Better than a no-op. + }, + maxTimings = longArrayOf(0L, 70L), + maxAmplitudes = intArrayOf(0, 180), + primaryPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + VibrationEffect.Composition.PRIMITIVE_QUICK_RISE + } else { + null + } + ), + JOYSTICK( + feedbackConstant = HapticFeedbackConstantsCompat.SEGMENT_FREQUENT_TICK, + maxTimings = longArrayOf(0L, 50L), + maxAmplitudes = intArrayOf(0, 128), + primaryPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + VibrationEffect.Composition.PRIMITIVE_LOW_TICK + } else { + null + }, + fallbackPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + VibrationEffect.Composition.PRIMITIVE_TICK + } else { + null + } + ); + + fun getMaxTimings(): LongArray = maxTimings.copyOf() + + @RequiresApi(Build.VERSION_CODES.O) + fun getMaxAmplitudes(): IntArray = maxAmplitudes.copyOf() + + fun scaleTimings(@FloatRange(from = 0.0, to = 1.0) scale: Float): LongArray = + maxTimings.map { (it.toFloat() * scale).toLong() }.toLongArray() + + @RequiresApi(Build.VERSION_CODES.O) + fun scaleAmplitudes(@FloatRange(from = 0.0, to = 1.0) scale: Float): IntArray = + maxAmplitudes.map { (it.toFloat() * scale).toInt() }.toIntArray() + + /** + * Gets this effect's preferred primitive ID that is supported by the provided [vibrator]. + * @param vibrator A [Vibrator] instance for checking whether primitives are supported by it. + * @return The best supported primitive ID of this effect, or null if there is none. + */ + @RequiresApi(Build.VERSION_CODES.R) + fun getSupportedPrimitive(vibrator: Vibrator): Int? = + listOf(primaryPrimitive, fallbackPrimitive).firstOrNull { + it != null && vibrator.areAllPrimitivesSupported(it) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt index 9b6bb23e05..418194cf4d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt @@ -31,7 +31,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForSIDevice import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForWiimoteSource -import org.dolphinemu.dolphinemu.utils.HapticEffect +import org.dolphinemu.dolphinemu.model.HapticEffect import org.dolphinemu.dolphinemu.utils.HapticsProvider import java.util.Arrays diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt index a3f6826295..71c7e2a709 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt @@ -12,7 +12,7 @@ import android.view.View import org.dolphinemu.dolphinemu.features.input.model.InputOverrider import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting -import org.dolphinemu.dolphinemu.utils.HapticEffect +import org.dolphinemu.dolphinemu.model.HapticEffect import org.dolphinemu.dolphinemu.utils.HapticsProvider import kotlin.math.atan2 import kotlin.math.cos diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt index b9c37e1d59..b865c9884f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt @@ -5,36 +5,40 @@ package org.dolphinemu.dolphinemu.utils import android.os.Build import android.os.VibrationEffect import android.os.Vibrator -import android.view.HapticFeedbackConstants import android.view.View import androidx.annotation.FloatRange -import androidx.annotation.RequiresApi +import androidx.core.view.ViewCompat import org.dolphinemu.dolphinemu.features.input.model.DolphinVibratorManagerFactory +import org.dolphinemu.dolphinemu.model.HapticEffect /** * Provides haptic feedback to the user. * * @property vibrator The [Vibrator] instance to be used for vibration. - * Defaults to the system default vibrator. + * Defaults to the system's default vibrator. */ class HapticsProvider( private val vibrator: Vibrator = DolphinVibratorManagerFactory.getSystemVibratorManager().getDefaultVibrator() ) { - private val primitiveSupport: Boolean = areAllPrimitivesSupported() - private val releaseHapticFeedbackConstant = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - HapticFeedbackConstants.VIRTUAL_KEY_RELEASE + private val supportedPrimitivesMap: Map + private val primitiveSupport: Boolean + + init { + supportedPrimitivesMap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + HapticEffect.entries.associateWith { it.getSupportedPrimitive(vibrator) } } else { - HapticFeedbackConstants.VIRTUAL_KEY // Same as press feedback is better than no feedback + HapticEffect.entries.associateWith { null } } + primitiveSupport = supportedPrimitivesMap.values.all { it != null } + } /** * Perform haptic feedback natively (if a [View] is provided), * or by composing primitives (if supported), falling back to a waveform or a legacy vibration. * * @param effect The [HapticEffect] of the feedback. - * @param view The [View] to perform the feedback on, can be null to vibrate using the [vibrator]. + * @param view The [View] to perform the feedback on, can be null to use the [vibrator] directly. * @param scale The intensity scale of the feedback, will only be used if [view] is not set. */ fun provideFeedback( @@ -43,105 +47,23 @@ class HapticsProvider( @FloatRange(from = 0.0, to = 1.0) scale: Float = 0.5f ) { if (view != null) { - view.performHapticFeedback(getHapticFeedbackConstant(effect)) - } else if (primitiveSupport) { + ViewCompat.performHapticFeedback(view, effect.feedbackConstant) + } else if (primitiveSupport && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { vibrator.vibrate( VibrationEffect .startComposition() - .addPrimitive(getPrimitive(effect), scale) + .addPrimitive(supportedPrimitivesMap[effect]!!, scale) .compose() ) } else { - val timings = getTimings(effect, scale) + val scaledTimings = effect.scaleTimings(scale) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (vibrator.hasAmplitudeControl()) { - vibrator.vibrate( - VibrationEffect.createWaveform( - timings, getAmplitudes(effect, scale), -1 - ) - ) - } else { - vibrator.vibrate(VibrationEffect.createWaveform(timings, -1)) - } + vibrator.vibrate( + VibrationEffect.createWaveform(scaledTimings, effect.scaleAmplitudes(scale), -1) + ) } else { - vibrator.vibrate(timings.sum()) + vibrator.vibrate(scaledTimings.sum()) } } } - - /** - * Get the timings for a waveform vibration based on the [effect], scaled by [scale]. - * - * @param effect The [HapticEffect] of the vibration. - * @param scale The intensity scale of the vibration. - * @return The LongArray of scaled timings for the specified [effect]. - */ - private fun getTimings( - effect: HapticEffect, @FloatRange(from = 0.0, to = 1.0) scale: Float - ): LongArray { - // Note: It is recommended that these values differ by a ratio of 1.4 or more, - // so the difference in the duration of the vibration can be easily perceived. - // Lower-end vibrators can't vibrate at all if the duration is too short. - return when (effect) { - HapticEffect.PRESS -> longArrayOf(0L, (100f * scale).toLong()) - HapticEffect.RELEASE -> longArrayOf(0L, (70f * scale).toLong()) - HapticEffect.JOYSTICK -> longArrayOf(0L, (50f * scale).toLong()) - } - } - - /** - * Get the amplitudes for a waveform vibration based on the [effect], scaled by [scale]. - * - * @param effect The [HapticEffect] of the vibration. - * @param scale The intensity scale of the vibration. - * @return The IntArray of scaled amplitudes for the specified [effect]. - */ - @RequiresApi(Build.VERSION_CODES.O) - private fun getAmplitudes( - effect: HapticEffect, @FloatRange(from = 0.0, to = 1.0) scale: Float - ): IntArray { - // Note: It is recommended that these values differ by a ratio of 1.4 or more, - // so the difference in the amplitude of the vibration can be easily perceived. - // Value range is between 0 and 255 (VibrationEffect.MAX_AMPLITUDE). - return when (effect) { - HapticEffect.PRESS -> intArrayOf(0, (255f * scale).toInt()) - HapticEffect.RELEASE -> intArrayOf(0, (180f * scale).toInt()) - HapticEffect.JOYSTICK -> intArrayOf(0, (128f * scale).toInt()) - } - } - - /** - * Get the haptic feedback constant that matches the [effect]. - * - * @param effect The [HapticEffect] of the feedback. - * @return The matching [HapticFeedbackConstants] constant. - */ - private fun getHapticFeedbackConstant(effect: HapticEffect): Int { - return when (effect) { - HapticEffect.PRESS -> HapticFeedbackConstants.VIRTUAL_KEY - HapticEffect.RELEASE -> releaseHapticFeedbackConstant - HapticEffect.JOYSTICK -> HapticFeedbackConstants.CLOCK_TICK - } - } - - @RequiresApi(Build.VERSION_CODES.S) - private fun getPrimitive(effect: HapticEffect): Int { - return when (effect) { - HapticEffect.PRESS -> VibrationEffect.Composition.PRIMITIVE_QUICK_FALL - HapticEffect.RELEASE -> VibrationEffect.Composition.PRIMITIVE_QUICK_RISE - HapticEffect.JOYSTICK -> VibrationEffect.Composition.PRIMITIVE_LOW_TICK - } - } - - private fun areAllPrimitivesSupported(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && vibrator.areAllPrimitivesSupported( - *HapticEffect.entries.map { getPrimitive(it) }.toIntArray() - ) - } -} - -enum class HapticEffect { - PRESS, - RELEASE, - JOYSTICK }