P2 Stream H: standalone TaskSuggestionsScreen
Port iOS TaskSuggestionsView as a standalone route reachable outside onboarding. Uses shared suggestions API + accept/skip analytics in non-onboarding variant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package com.tt.honeyDue.ui.haptics
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* Android backend using [HapticFeedbackConstants] when a host [View] is available,
|
||||
* with graceful [Vibrator] fallback for older APIs or headless contexts.
|
||||
*
|
||||
* API-30+ (Android 11+) gets the richer CONFIRM / REJECT / GESTURE_END constants.
|
||||
* Pre-30 falls back to predefined [VibrationEffect]s (EFFECT_TICK, EFFECT_CLICK,
|
||||
* EFFECT_HEAVY_CLICK) on API 29+, or one-shot vibrations on API 26–28,
|
||||
* or legacy Vibrator.vibrate(duration) on pre-26.
|
||||
*
|
||||
* Call [HapticsInit.install] from your Application / MainActivity so the app
|
||||
* context is available for vibrator resolution. Without it, the backend is
|
||||
* silently a no-op (never crashes).
|
||||
*/
|
||||
class AndroidDefaultHapticBackend(
|
||||
private val viewProvider: () -> View? = { null },
|
||||
private val vibratorProvider: () -> Vibrator? = { null }
|
||||
) : HapticBackend {
|
||||
|
||||
override fun perform(event: HapticEvent) {
|
||||
val view = viewProvider()
|
||||
if (view != null && performViaView(view, event)) return
|
||||
performViaVibrator(event)
|
||||
}
|
||||
|
||||
private fun performViaView(view: View, event: HapticEvent): Boolean {
|
||||
val constant = when (event) {
|
||||
HapticEvent.LIGHT -> HapticFeedbackConstants.CONTEXT_CLICK
|
||||
HapticEvent.MEDIUM -> HapticFeedbackConstants.KEYBOARD_TAP
|
||||
HapticEvent.HEAVY -> HapticFeedbackConstants.LONG_PRESS
|
||||
HapticEvent.SUCCESS -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
HapticFeedbackConstants.CONFIRM
|
||||
} else {
|
||||
HapticFeedbackConstants.CONTEXT_CLICK
|
||||
}
|
||||
HapticEvent.WARNING -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
HapticFeedbackConstants.GESTURE_END
|
||||
} else {
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
}
|
||||
HapticEvent.ERROR -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
HapticFeedbackConstants.REJECT
|
||||
} else {
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
}
|
||||
}
|
||||
return view.performHapticFeedback(constant)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun performViaVibrator(event: HapticEvent) {
|
||||
val vibrator = vibratorProvider() ?: return
|
||||
if (!vibrator.hasVibrator()) return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val predefined = when (event) {
|
||||
HapticEvent.LIGHT, HapticEvent.SUCCESS -> VibrationEffect.EFFECT_TICK
|
||||
HapticEvent.MEDIUM, HapticEvent.WARNING -> VibrationEffect.EFFECT_CLICK
|
||||
HapticEvent.HEAVY, HapticEvent.ERROR -> VibrationEffect.EFFECT_HEAVY_CLICK
|
||||
}
|
||||
vibrator.vibrate(VibrationEffect.createPredefined(predefined))
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val (duration, amplitude) = when (event) {
|
||||
HapticEvent.LIGHT -> 10L to VibrationEffect.DEFAULT_AMPLITUDE
|
||||
HapticEvent.MEDIUM -> 20L to VibrationEffect.DEFAULT_AMPLITUDE
|
||||
HapticEvent.HEAVY -> 50L to VibrationEffect.DEFAULT_AMPLITUDE
|
||||
HapticEvent.SUCCESS -> 30L to VibrationEffect.DEFAULT_AMPLITUDE
|
||||
HapticEvent.WARNING -> 40L to VibrationEffect.DEFAULT_AMPLITUDE
|
||||
HapticEvent.ERROR -> 60L to VibrationEffect.DEFAULT_AMPLITUDE
|
||||
}
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
|
||||
return
|
||||
}
|
||||
|
||||
val duration = when (event) {
|
||||
HapticEvent.LIGHT -> 10L
|
||||
HapticEvent.MEDIUM -> 20L
|
||||
HapticEvent.HEAVY -> 50L
|
||||
HapticEvent.SUCCESS -> 30L
|
||||
HapticEvent.WARNING -> 40L
|
||||
HapticEvent.ERROR -> 60L
|
||||
}
|
||||
vibrator.vibrate(duration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Android app-wide registry that plumbs an Application Context to the default
|
||||
* backend. Call [HapticsInit.install] from the Application or Activity init so
|
||||
* that call-sites in shared code can invoke [Haptics.light] etc. without any
|
||||
* Compose / View plumbing.
|
||||
*/
|
||||
object HapticsInit {
|
||||
@Volatile private var appContext: Context? = null
|
||||
@Volatile private var hostView: View? = null
|
||||
|
||||
fun install(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
fun attachView(view: View?) {
|
||||
hostView = view
|
||||
}
|
||||
|
||||
internal fun defaultBackend(): HapticBackend = AndroidDefaultHapticBackend(
|
||||
viewProvider = { hostView },
|
||||
vibratorProvider = { resolveVibrator() }
|
||||
)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun resolveVibrator(): Vibrator? {
|
||||
val ctx = appContext ?: return null
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
(ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
||||
} else {
|
||||
ctx.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actual object Haptics {
|
||||
@Volatile private var backend: HapticBackend = HapticsInit.defaultBackend()
|
||||
|
||||
actual fun light() = backend.perform(HapticEvent.LIGHT)
|
||||
actual fun medium() = backend.perform(HapticEvent.MEDIUM)
|
||||
actual fun heavy() = backend.perform(HapticEvent.HEAVY)
|
||||
actual fun success() = backend.perform(HapticEvent.SUCCESS)
|
||||
actual fun warning() = backend.perform(HapticEvent.WARNING)
|
||||
actual fun error() = backend.perform(HapticEvent.ERROR)
|
||||
|
||||
actual fun setBackend(backend: HapticBackend) {
|
||||
this.backend = backend
|
||||
}
|
||||
|
||||
actual fun resetBackend() {
|
||||
this.backend = HapticsInit.defaultBackend()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user