androidwidgetandroid-jetpackandroid-vibrationglance

How to add haptics to Button in home screen widget (Glance API) in android


I'm trying to make a homescreen widget (simply a button) that cycles through two wallpapers i have, and I want to add haptic feedback to it. I read the glance APIs have limited compose functionality so calling a function that triggers haptic feedback is not working in the below example. but I saw my Nothing phone's compass widget that gives haptic feedback when facing north, so I know it's at least possible. could you please guide me through this on how I can achieve this effect, kinda new to Android development.

// desktop widget to quickly toggle wallpaper
object WallChangeWidget : GlanceAppWidget() {
    val wallVariant = intPreferencesKey("wallVariant")
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            Button(text = "click", onClick = hapticsAndThenAction())
        }
    }

    @Composable
    fun hapticsAndThenAction(): Action {
        val haptic = LocalHapticFeedback.current
        haptic.performHapticFeedback(HapticFeedbackType.LongPress)
        return actionRunCallback(ChangeWallActionCallback::class.java)
    }
}

here's the ActionCallback that is triggered to swap the wallpapers

object ChangeWallActionCallback : ActionCallback {
    @RequiresApi(Build.VERSION_CODES.R)
    override suspend fun onAction(
        context: Context,
        glanceId: GlanceId,
        parameters: ActionParameters
    ) {
        updateAppWidgetState(context, glanceId) { prefs ->
            val currentCount = prefs[WallChangeWidget.wallVariant]
            val x = WallpaperManager.getInstance(context)
            if (currentCount != 1) {
                val bmp: Bitmap =
                    BitmapFactory.decodeFile("/storage/emulated/0/Media/Pictures/WallSwitch/two.png")
                x.setBitmap(bmp)
                prefs[WallChangeWidget.wallVariant] = 1
            } else {

                val bmp: Bitmap =
                    BitmapFactory.decodeFile("/storage/emulated/0/Media/Pictures/WallSwitch/one.png")
                x.setBitmap(bmp)
//                x.clear()
                prefs[WallChangeWidget.wallVariant] = 0
            }
        }
    }
}

the above code gives me the following exception, stating that of course this is not possible as LocalHapticFeedback is not present.

2024-03-23 21:56:31.511 32147-32183 WM-WorkerWrapper        com.sliya.np.ext.wallswitch          E  Work [ id=0f9eb632-47c1-4e38-bd06-86acbf66c988, tags={ androidx.glance.session.SessionWorker } ] failed because it threw an exception/error
java.util.concurrent.ExecutionException: java.lang.IllegalStateException: CompositionLocal LocalHapticFeedback not present
at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:516)
at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:475)
at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:311)
at androidx.work.impl.utils.SerialExecutor$Task.run(SerialExecutor.java:91)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
at java.lang.Thread.run(Thread.java:1012)
Caused by: java.lang.IllegalStateException: CompositionLocal LocalHapticFeedback not present
at androidx.compose.ui.platform.CompositionLocalsKt.noLocalProvidedFor(CompositionLocals.kt:220)
at androidx.compose.ui.platform.CompositionLocalsKt.access$noLocalProvidedFor(CompositionLocals.kt:1)
at androidx.compose.ui.platform.CompositionLocalsKt$LocalHapticFeedback$1.invoke(CompositionLocals.kt:117)
at androidx.compose.ui.platform.CompositionLocalsKt$LocalHapticFeedback$1.invoke(CompositionLocals.kt:116)
at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
at androidx.compose.runtime.LazyValueHolder.getCurrent(ValueHolders.kt:29)
at androidx.compose.runtime.LazyValueHolder.getValue(ValueHolders.kt:31)
at androidx.compose.runtime.CompositionLocalMapKt.read(CompositionLocalMap.kt:88)
at androidx.compose.runtime.ComposerImpl.consume(Composer.kt:2049)
at com.sliya.np.ext.wallswitch.WallChangeWidget.MyButton(WallChangeWidget.kt:90)
at com.sliya.np.ext.wallswitch.ComposableSingletons$WallChangeWidgetKt$lambda-1$1.invoke(WallChangeWidget.kt:34)
at com.sliya.np.ext.wallswitch.ComposableSingletons$WallChangeWidgetKt$lambda-1$1.invoke(WallChangeWidget.kt:33)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.glance.appwidget.SizeBoxKt$SizeBox$1.invoke(SizeBox.kt:127)
at androidx.glance.appwidget.SizeBoxKt$SizeBox$1.invoke(SizeBox.kt:74)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.glance.appwidget.SizeBoxKt.SizeBox-IbIYxLY(SizeBox.kt:74)
at androidx.glance.appwidget.SizeBoxKt.ForEachSize-eVKgIn8(SizeBox.kt:114)
at androidx.glance.appwidget.AppWidgetSession$provideGlance$1$1.invoke(AppWidgetSession.kt:110)
at androidx.glance.appwidget.AppWidgetSession$provideGlance$1$1.invoke(AppWidgetSession.kt:90)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.glance.appwidget.AppWidgetSession$provideGlance$1.invoke(AppWidgetSession.kt:85)
at androidx.glance.appwidget.AppWidgetSession$provideGlance$1.invoke(AppWidgetSession.kt:84)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:169)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2468)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2737)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3352)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3303)
2024-03-23 21:56:31.517 32147-32183 WM-WorkerWrapper        com.sliya.np.ext.wallswitch          E  at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:781)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1097)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:124)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:569)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:537)
at androidx.compose.runtime.BroadcastFrameClock$FrameAwaiter.resume(BroadcastFrameClock.kt:42)
at androidx.compose.runtime.BroadcastFrameClock.sendFrame(BroadcastFrameClock.kt:71)
at androidx.glance.session.InteractiveFrameClock.sendFrame(InteractiveFrameClock.kt:127)
at androidx.glance.session.InteractiveFrameClock.access$sendFrame(InteractiveFrameClock.kt:39)
at androidx.glance.session.InteractiveFrameClock$onNewAwaiters$2.invokeSuspend(InteractiveFrameClock.kt:117)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:334)
at android.app.ActivityThread.main(ActivityThread.java:8293)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1053)

Solution

  • deriving from @CommonsWave's answer calling vibrate from ActionCallback worked.

    object ChangeWallActionCallback : ActionCallback {
        override suspend fun onAction(
            context: Context,
            glanceId: GlanceId,
            parameters: ActionParameters
        ) {
    
         
            val vibrator = context.getSystemService(Vibrator::class.java)
            vibrator.vibrate(
                VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK),
                VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_NOTIFICATION).build()
            )
    
    // other stuff
    
    }