Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 0 additions & 4 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,244 +1,97 @@
package io.homeassistant.companion.android.widgets.template

import android.appwidget.AppWidgetManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BaseActivity
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.databinding.WidgetTemplateConfigureBinding
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
import io.homeassistant.companion.android.util.applySafeDrawingInsets
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
import io.homeassistant.companion.android.util.getHexForColor
import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity
import io.homeassistant.companion.android.widgets.common.WidgetUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import timber.log.Timber

// TODO Migrate to compose https://github.com/home-assistant/android/issues/6304
@AndroidEntryPoint
class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity<TemplateWidgetEntity, TemplateWidgetDao>() {
private lateinit var binding: WidgetTemplateConfigureBinding
class TemplateWidgetConfigureActivity : BaseActivity() {
Comment thread
amlwin marked this conversation as resolved.

override val serverSelect: View
get() = binding.serverSelect
private val viewModel: TemplateWidgetConfigureViewModel by viewModels()

override val serverSelectList: Spinner
get() = binding.serverSelectList

private var requestLauncherSetup = false
private val supportedTextColors: List<String>
get() = listOf(
application.getHexForColor(commonR.color.colorWidgetButtonLabelBlack),
application.getHexForColor(android.R.color.white),
)
Comment on lines +22 to +26
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TemplateWidgetConfigureView shows text color choices in the order Black then White, and the Activity passes supportedTextColors in the same order. In the previous XML, the White option was checked by default for transparent widgets; with the new setup the default becomes Black (index 0). If this is unintended, reorder the options or default textColorIndex to preserve the prior default behavior.

Copilot uses AI. Check for mistakes.

public override fun onCreate(savedInstanceState: Bundle?) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Set the result to CANCELED. This will cause the widget host to cancel
// out of the widget placement if the user presses the back button.
setResult(RESULT_CANCELED)

binding = WidgetTemplateConfigureBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.root.applySafeDrawingInsets()

// Find the widget id from the intent.
val intent = intent
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID,
)
requestLauncherSetup = extras.getBoolean(
ManageWidgetsViewModel.CONFIGURE_REQUEST_LAUNCHER,
false,
)
}
val widgetId = extras?.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID,
) ?: AppWidgetManager.INVALID_APPWIDGET_ID

val requestLauncherSetup = extras?.getBoolean(
ManageWidgetsViewModel.CONFIGURE_REQUEST_LAUNCHER,
false,
) ?: false

// If this activity was started with an intent without an app widget ID, finish with an error.
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !requestLauncherSetup) {
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !requestLauncherSetup) {
finish()
return
}

val backgroundTypeValues = WidgetUtils.getBackgroundOptionList(this)
binding.backgroundType.adapter =
ArrayAdapter(
this,
android.R.layout.simple_spinner_dropdown_item,
backgroundTypeValues,
)

lifecycleScope.launch {
val templateWidget = dao.get(appWidgetId)

if (templateWidget?.serverId != null) {
// Set server ID early for template rendering
selectedServerId = templateWidget.serverId
}
setupServerSelect(templateWidget?.serverId)
viewModel.onSetup(widgetId = widgetId, supportedTextColors = supportedTextColors)

if (templateWidget != null) {
binding.templateText.setText(templateWidget.template)
binding.textSize.setText(templateWidget.textSize.toInt().toString())
binding.addButton.setText(commonR.string.update_widget)
if (templateWidget.template.isNotEmpty()) {
renderTemplateText(templateWidget.template)
} else {
binding.renderedTemplate.text = getString(commonR.string.empty_template)
binding.addButton.isEnabled = false
}
binding.backgroundType.setSelection(
WidgetUtils.getSelectedBackgroundOption(
this@TemplateWidgetConfigureActivity,
templateWidget.backgroundType,
backgroundTypeValues,
),
setContent {
HomeAssistantAppTheme {
TemplateWidgetConfigureScreen(
viewModel = viewModel,
onActionClick = { onActionClick(requestLauncherSetup) },
)
binding.textColor.isVisible = templateWidget.backgroundType == WidgetBackgroundType.TRANSPARENT
binding.textColorWhite.isChecked =
templateWidget.textColor?.let {
it.toColorInt() == ContextCompat.getColor(
this@TemplateWidgetConfigureActivity,
android.R.color.white,
)
}
?: true
binding.textColorBlack.isChecked =
templateWidget.textColor?.let {
it.toColorInt() ==
ContextCompat.getColor(
this@TemplateWidgetConfigureActivity,
commonR.color.colorWidgetButtonLabelBlack,
)
}
?: false
} else {
binding.backgroundType.setSelection(0)
}
}

binding.templateText.doAfterTextChanged { renderTemplateText() }

binding.backgroundType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
binding.textColor.isVisible =
parent?.adapter?.getItem(position) == getString(commonR.string.widget_background_type_transparent)
}

override fun onNothingSelected(parent: AdapterView<*>?) {
binding.textColor.visibility = View.GONE
}
}
}

binding.addButton.setOnClickListener {
private fun onActionClick(requestLauncherSetup: Boolean) {
lifecycleScope.launch {
if (requestLauncherSetup) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
lifecycleScope.launch {
requestWidgetCreation()
}
} else {
showAddWidgetError() // this shouldn't be possible
}
requestPinWidget()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not checking the version anymore? If you do so it would remove the warning in the ViewModel by properly annotating the function requestPinWidget like it is done in TodoWidgetConfigure.

} else {
lifecycleScope.launch {
updateWidget()
}
onUpdateWidget()
}
}
}

override fun onServerSelected(serverId: Int) = renderTemplateText()

override suspend fun getPendingDaoEntity(): TemplateWidgetEntity {
val serverId = checkNotNull(selectedServerId) { "Selected server ID is null" }
val template = checkNotNull(binding.templateText.text?.toString()) { "Template text is null" }

return TemplateWidgetEntity(
id = appWidgetId,
serverId = serverId,
template = template,
textSize = binding.textSize.text.toString().toFloat(),
backgroundType = when (binding.backgroundType.selectedItem as String?) {
getString(commonR.string.widget_background_type_dynamiccolor) -> WidgetBackgroundType.DYNAMICCOLOR
getString(commonR.string.widget_background_type_transparent) -> WidgetBackgroundType.TRANSPARENT
else -> WidgetBackgroundType.DAYNIGHT
},
textColor = if (binding.backgroundType.selectedItem as String? ==
getString(commonR.string.widget_background_type_transparent)
) {
getHexForColor(
if (binding.textColorWhite.isChecked) {
android.R.color.white
} else {
commonR.color.colorWidgetButtonLabelBlack
},
)
} else {
null
},
lastUpdate = dao.get(appWidgetId)?.lastUpdate ?: "Loading",
)
}

override val widgetClass: Class<*> = TemplateWidget::class.java

private fun renderTemplateText() {
val editableText = binding.templateText.text ?: return
if (editableText.isNotEmpty()) {
renderTemplateText(editableText.toString())
} else {
binding.renderedTemplate.text = getString(commonR.string.empty_template)
binding.addButton.isEnabled = false
private suspend fun requestPinWidget() {
try {
viewModel.requestWidgetCreation(this@TemplateWidgetConfigureActivity)
finish()
Comment on lines +63 to +76
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestPinWidget() is called without an API 26+ guard, but AppWidgetManager.requestPinAppWidget only exists on O+. On pre-O devices this path can crash with NoSuchMethodError. Add a Build.VERSION.SDK_INT >= Build.VERSION_CODES.O check (as other widget configure activities do) or mark the call site @RequiresApi(26) and ensure it’s never invoked below 26.

Copilot uses AI. Check for mistakes.
} catch (e: IllegalStateException) {
if (e is CancellationException) throw e
Comment on lines +77 to +78
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (e: IllegalStateException) {
if (e is CancellationException) throw e
} catch (e: CancellationException) {
throw e
} catch (e: IllegalStateException) {

This is way we use in the repo, please adjust in the activity and the viewModel

showAddWidgetError()
}
}

private fun renderTemplateText(template: String) {
val serverId = selectedServerId
if (serverId == null) {
Timber.w("Not rendering template because server is not set")
return
private suspend fun onUpdateWidget() {
try {
viewModel.updateWidgetConfiguration(this@TemplateWidgetConfigureActivity)
setResult(RESULT_OK)
finish()
} catch (e: IllegalStateException) {
Comment on lines +83 to +88
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When finishing widget configuration (non-launcher flow), the result should include AppWidgetManager.EXTRA_APPWIDGET_ID so the launcher/host knows which widget was configured. Previously this was handled by BaseWidgetConfigureActivity.updateWidget(); now only setResult(RESULT_OK) is returned, which can cause the widget host to treat configuration as failed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 👍🏻, I'm not sure it is needed in the TodoWidgetConfigure because we use Glance and we manually call the update method from the manager.

if (e is CancellationException) throw e
showAddWidgetError()
}
Comment on lines +73 to 91
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both requestPinWidget() and onUpdateWidget() catch IllegalStateException and then check if (e is CancellationException), but e can never be a CancellationException due to the catch type. Either catch Exception (and rethrow CancellationException) or remove the unreachable cancellation check.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the if no need if you only catch IllegalStateException. The cancelation is already thrown

}

lifecycleScope.launch {
var templateText: String?
var enabled: Boolean
withContext(Dispatchers.IO) {
try {
templateText =
serverManager.integrationRepository(serverId)
.renderTemplate(template, mapOf())
.toString()
enabled = true
} catch (e: Exception) {
Timber.e(e, "Exception while rendering template")
// SerializationException suggests that template is not a String (= error)
templateText = getString(
if (e.cause is SerializationException) {
commonR.string.template_error
} else {
commonR.string.template_render_error
},
Comment on lines -229 to -234
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is a good idea to keep this logic that was giving a bit more details to the user making the template on what is the current error.

)
enabled = false
}
}
binding.renderedTemplate.text =
templateText?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY) }
binding.addButton.isEnabled = enabled && isValidServerId()
}
private fun showAddWidgetError() {
viewModel.showError(commonR.string.widget_creation_error)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error is always the same and always trigger from the activity I propose you to not use the viewModel and directly send this to the screen, it would simplify things.

}
}
Loading
Loading