androidkotlinvibrationandroid-vibration

Android VibrationManager cancel is not stopping vibration


I have the following code that is supposed to start and stop vibration in my application:

fun Context.vibrate(wave: Boolean) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        (getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).apply {
            vibrate(
                CombinedVibration.createParallel(makeEffect(wave)),
                VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build(),
            )
        }
    } else {
        @Suppress("DEPRECATION")
        (getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).apply {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                vibrate(makeEffect(wave))
            } else {
                @Suppress("DEPRECATION")
                vibrate(if (wave) 1000 else 500)
            }
        }
    }
}

@RequiresApi(Build.VERSION_CODES.O)
private fun makeEffect(wave: Boolean): VibrationEffect {
    return if (wave) {
        VibrationEffect.createWaveform(timeInterval, 1)
    } else {
        VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE)
    }
}

fun Context.stopVibration(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        (getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).apply {
            cancel()
        }
    } else {
        @Suppress("DEPRECATION")
        (getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).apply {
            if (hasVibrator()) {
                cancel()
            }
        }
    }
}

private val timeInterval = longArrayOf(60, 120, 180, 240, 420, 480)

The problem is that this code does not actually stop the vibration. Instead, the vibration continues until I kill the app.

Why doesn't it stop? How can I fix it?


Solution

  • The VibratorManager and Vibrator documentation doesn't say this anywhere, but those two classes maintain some internal state - they don't just rely on determining the current vibrator hardware status.

    Therefore, creating a new object and calling cancel on it does not work, because that object doesn't think anything needs to be done.

    My solution is to have my vibrate method return an object with a stop method that points to the original object's cancel method:

    sealed class VibratorCompat {
        abstract fun stop()
    
        @RequiresApi(Build.VERSION_CODES.S)
        class Impl31(private val manager: VibratorManager): VibratorCompat() {
            override fun stop() {
                manager.cancel()
            }
        }
    
        class ImplLegacy(private val vibrator: Vibrator): VibratorCompat() {
            override fun stop() {
                vibrator.cancel()
            }
        }
    }
    

    I return it like this:

    fun Context.vibrate(wave: Boolean): VibratorCompat {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            return VibratorCompat.Impl31((getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).apply {
                vibrate(
                    CombinedVibration.createParallel(makeEffect(wave)),
                    VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_ALARM).build(),
                )
            })
        } else {
            @Suppress("DEPRECATION")
            return VibratorCompat.ImplLegacy((getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).apply {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    vibrate(makeEffect(wave))
                } else {
                    @Suppress("DEPRECATION")
                    vibrate(if (wave) 1000 else 500)
                }
            })
        }
    }