Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
79d2f47
Add Compose UI Elements for Widget Button Configure Widget
BFadairo Mar 3, 2026
d4ca03a
Create ViewModel For ButtonWidgetConfigureActivity
BFadairo Mar 5, 2026
b7ae653
Create AlertDialog for Adding Fields
BFadairo Mar 12, 2026
1f6d05d
Refactor ViewModel & Composables to use ButtonWidgetUiState
BFadairo Mar 13, 2026
b6eca38
Add Filtering to Action Text Input Field
BFadairo Apr 27, 2026
53e2edc
Modify Action Text Field to use TextFieldState
BFadairo Apr 27, 2026
0a9f1e7
Update Dynamic Fields Text Input to track state
BFadairo Apr 27, 2026
90a69be
Add Widget Creation
BFadairo Apr 28, 2026
6cd5bee
Move Action Input Field to it's own Composable
BFadairo May 19, 2026
fb7af53
Fix Action Text not being restored when updating widget
BFadairo May 22, 2026
8dbd025
Add Entity Selection to Entity id dynamic field also update Add Butto…
BFadairo May 22, 2026
97b104c
Update UI and add Tap action to Button Widget
BFadairo May 22, 2026
93fb624
Merge branch 'main' into rework/button_config_activity_compose
BFadairo Jun 3, 2026
3802f24
Update App Widget State on tap
BFadairo Jun 12, 2026
a2bceaf
Add States for Error & Success
BFadairo Jun 12, 2026
c63314f
Merge branch 'main' into rework/button_config_activity_compose
BFadairo Jun 12, 2026
874f9cf
Modify Image to Properly adjust when expanding Widget
BFadairo Jul 23, 2026
f7cb4f6
Merge branch 'main' into rework/button_config_activity_compose
BFadairo Jul 23, 2026
f978b80
Ran ktlint
BFadairo Jul 23, 2026
b8aa8c3
Fix Build errors
BFadairo Jul 23, 2026
c9ce7db
lint
BFadairo Jul 23, 2026
4745a51
Merge branch 'main' into rework/button_config_activity_compose
BFadairo Jul 25, 2026
c3db459
Remove unused resources
BFadairo Jul 25, 2026
9ffcc99
Merge branch 'main' into rework/button_config_activity_compose
BFadairo Jul 27, 2026
7c3df0e
Fix Actions not running properly
BFadairo Jul 28, 2026
f6a3004
Update Icon Color
BFadairo Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ open class HomeAssistantApplication :

if (!isAutomotive()) {
// Update widgets when the screen turns on, updates are skipped if widgets were not added
val buttonWidget = ButtonWidget()
ButtonWidget().registerReceiver(this)
val entityWidget = EntityWidget()
val mediaPlayerWidget = MediaPlayerControlsWidget()
val templateWidget = TemplateWidget()
Expand All @@ -355,7 +355,6 @@ open class HomeAssistantApplication :
screenIntentFilter.addAction(Intent.ACTION_SCREEN_ON)
screenIntentFilter.addAction(Intent.ACTION_SCREEN_OFF)

ContextCompat.registerReceiver(this, buttonWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, entityWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(
this,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package io.homeassistant.companion.android.widgets.button

import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.toColorInt
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.action.actionSendBroadcast

Check failure

Code scanning / ktlint

Unused import Error

Unused import

Check failure

Code scanning / ktlint

Unused import Error

Unused import
import androidx.glance.appwidget.action.actionStartActivity

Check failure

Code scanning / ktlint

Unused import Error

Unused import

Check failure

Code scanning / ktlint

Unused import Error

Unused import
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.color.ColorProviders
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.material.ColorProviders
import androidx.glance.text.Text
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.IconicsSize
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.padding
import com.mikepenz.iconics.utils.size
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.util.compose.HomeAssistantGlanceTheme
import io.homeassistant.companion.android.util.compose.HomeAssistantGlanceTypography
import io.homeassistant.companion.android.util.compose.glanceHaLightColors
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
import io.homeassistant.companion.android.widgets.button.ButtonWidget.Companion.CALL_SERVICE
import io.homeassistant.companion.android.widgets.button.ButtonWidget.Companion.CALL_SERVICE_AUTH
import io.homeassistant.companion.android.widgets.button.ButtonWidget.Companion.DEFAULT_MAX_ICON_SIZE
import timber.log.Timber

private val authKey = ActionParameters.Key<Boolean>("auth")
private val widgetIdKey = ActionParameters.Key<Int>("widgetId")
class ButtonGlanceAppWidget : GlanceAppWidget() {
@EntryPoint
@InstallIn(SingletonComponent::class)
internal interface ButtonGlanceWidgetEntryPoint {
fun buttonStateUpdater(): ButtonWidgetStateUpdater
}

override suspend fun provideGlance(context: Context, id: GlanceId) {
val manager = GlanceAppWidgetManager(context)
val widgetId = manager.getAppWidgetId(id)

provideContent {
val entryPoints = remember { EntryPoints.get(context, ButtonGlanceWidgetEntryPoint::class.java) }
val flow = remember { entryPoints.buttonStateUpdater().getButtonEntityFlow(widgetId) }

val state by flow.collectAsState(null)

Timber.i("GlanceAppWidget $state")
HomeAssistantGlanceTheme(colors = getWidgetColors(state?.backgroundType, state?.textColor)) {
ButtonScreen(context, state, DEFAULT_MAX_ICON_SIZE)
}
}
}
}

@Composable
private fun GlanceModifier.buttonWidgetBackground(): GlanceModifier {
return this.appWidgetBackground().fillMaxSize().background(
GlanceTheme
.colors.widgetBackground,
)
}

@Composable
fun ButtonScreen(context: Context, state: ButtonWidgetEntity?, maxIconSize: Int, modifier: Modifier = Modifier) {
val iconData = state?.iconName?.let { CommunityMaterial.getIconByMdiName(it) }
?: CommunityMaterial.Icon2.cmd_flash // Lightning Bolt
val iconDrawable = IconicsDrawable(context, iconData).apply {
padding = IconicsSize.dp(2)
size = IconicsSize.dp(24)
}

// Determine reasonable dimensions for drawing vector icon as a bitmap
val aspectRatio = iconDrawable.intrinsicWidth / iconDrawable.intrinsicHeight.toDouble()
val awo = if (state != null) AppWidgetManager.getInstance(context).getAppWidgetOptions(state.id ?: 0) else null
val maxWidth = (
awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxIconSize)
?: maxIconSize
).coerceAtLeast(16)
val maxHeight = (
awo?.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxIconSize)
?: maxIconSize
).coerceAtLeast(16)
val width: Int
val height: Int
if (maxWidth > maxHeight) {
width = maxWidth
height = (maxWidth * (1 / aspectRatio)).toInt()
} else {
width = (maxHeight * aspectRatio).toInt()
height = maxHeight
}

val icon = DrawableCompat.wrap(iconDrawable).toBitmap(width, height)

val requiresAuth = state?.requireAuthentication

Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
modifier = GlanceModifier.fillMaxSize().buttonWidgetBackground()
.clickable(onClick = actionRunCallback<TapAction>(

Check failure

Code scanning / ktlint

Missing newline after "(" Error

Missing newline after "("

Check failure

Code scanning / ktlint

Missing newline after "(" Error

Missing newline after "("
actionParametersOf(
authKey to (requiresAuth == true),
widgetIdKey to (state?.id ?: -1)

Check failure

Code scanning / ktlint

Missing trailing comma before ")" Error

Missing trailing comma before ")"

Check failure

Code scanning / ktlint

Missing trailing comma before ")" Error

Missing trailing comma before ")"
)

Check failure

Code scanning / ktlint

Missing trailing comma before ")" Error

Missing trailing comma before ")"

Check failure

Code scanning / ktlint

Missing trailing comma before ")" Error

Missing trailing comma before ")"
)),

Check failure

Code scanning / ktlint

Missing newline before ")" Error

Missing newline before ")"

Check failure

Code scanning / ktlint

Missing newline before ")" Error

Missing newline before ")"
) {
Image(
provider = ImageProvider(icon),
contentDescription = null,
)
state?.label?.let {
Text(text = it, style = HomeAssistantGlanceTypography.bodySmall)
}
}
}

@Composable
fun getWidgetColors(
backgroundType: WidgetBackgroundType?,
textColor: String?,
modifier: Modifier = Modifier,
): ColorProviders {
return when (backgroundType) {
WidgetBackgroundType.DYNAMICCOLOR -> GlanceTheme.colors
WidgetBackgroundType.DAYNIGHT -> HomeAssistantGlanceTheme.colors
WidgetBackgroundType.TRANSPARENT -> ColorProviders(
glanceHaLightColors
.copy(
background = Color.Transparent,
onSurface = Color(
textColor?.toColorInt() ?: glanceHaLightColors.onSurface.toArgb(),
),
),
)

else -> HomeAssistantGlanceTheme.colors
}
}

class TapAction : ActionCallback {
override suspend fun onAction(
context: Context,

Check failure

Code scanning / ktlint

No whitespace expected between opening parenthesis and first parameter name Error

No whitespace expected between opening parenthesis and first parameter name

Check failure

Code scanning / ktlint

No whitespace expected between opening parenthesis and first parameter name Error

No whitespace expected between opening parenthesis and first parameter name
glanceId: GlanceId,

Check failure

Code scanning / ktlint

Single whitespace expected before parameter Error

Single whitespace expected before parameter

Check failure

Code scanning / ktlint

Single whitespace expected before parameter Error

Single whitespace expected before parameter
parameters: ActionParameters,

Check failure

Code scanning / ktlint

Single whitespace expected before parameter Error

Single whitespace expected before parameter

Check failure

Code scanning / ktlint

Single whitespace expected before parameter Error

Single whitespace expected before parameter

Check failure

Code scanning / ktlint

No whitespace expected between last parameter and closing parenthesis Error

No whitespace expected between last parameter and closing parenthesis

Check failure

Code scanning / ktlint

No whitespace expected between last parameter and closing parenthesis Error

No whitespace expected between last parameter and closing parenthesis
) {
val auth = parameters[authKey] ?: false
val widgetId = parameters[widgetIdKey]
val intent = Intent(context, ButtonWidget::class.java).apply {
action = if (auth) CALL_SERVICE_AUTH else CALL_SERVICE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
}
Timber.i("Action Running, Id: $widgetId, auth: $auth")
context.sendBroadcast(intent)
ButtonGlanceAppWidget().update(context, glanceId)
}
}
Loading
Loading