Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -1,245 +1,79 @@
package io.homeassistant.companion.android.widgets.template

import android.appwidget.AppWidgetManager
import android.os.Build
import android.content.Intent
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.common.R as commonR
import io.homeassistant.companion.android.common.util.SdkVersion
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 dagger.hilt.android.lifecycle.withCreationCallback
import io.homeassistant.companion.android.BaseActivity
import io.homeassistant.companion.android.common.compose.theme.HATheme
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
import io.homeassistant.companion.android.util.applySafeDrawingInsets
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.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

override val serverSelect: View
get() = binding.serverSelect

override val serverSelectList: Spinner
get() = binding.serverSelectList
class TemplateWidgetConfigureActivity : BaseActivity() {

private val widgetId: Int
get() = intent.extras?.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID,
) ?: AppWidgetManager.INVALID_APPWIDGET_ID

private val viewModel: TemplateWidgetConfigureViewModel by viewModels(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<TemplateWidgetConfigureViewModel.Factory> { factory ->
factory.create(widgetId)
}
},
)

private var requestLauncherSetup = false
private val requestLauncherSetup: Boolean
get() = intent.extras?.getBoolean(ManageWidgetsViewModel.CONFIGURE_REQUEST_LAUNCHER, false) == true

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,
)
}

// 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)

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 {
HATheme {
TemplateWidgetConfigureScreen(
viewModel = viewModel,
// The app sets the extra when it opens this screen itself, so there is
// something to go back to. The launcher opens it through the
// APPWIDGET_CONFIGURE filter instead, leaving nothing behind us.
canNavigateBack = requestLauncherSetup,
onNavigate = ::finish,
onActionClick = ::onActionClick,
)
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() {
lifecycleScope.launch {
if (requestLauncherSetup) {
if (SdkVersion.isAtLeast(Build.VERSION_CODES.O)) {
lifecycleScope.launch {
requestWidgetCreation()
}
} else {
showAddWidgetError() // this shouldn't be possible
}
} else {
lifecycleScope.launch {
updateWidget()
if (viewModel.requestWidgetCreation(this@TemplateWidgetConfigureActivity)) {
finish()
}
}
}
}

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 fun renderTemplateText(template: String) {
val serverId = selectedServerId
if (serverId == null) {
Timber.w("Not rendering template because server is not set")
return
}

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
},
if (viewModel.updateWidgetConfiguration()) {
viewModel.updateWidget(this@TemplateWidgetConfigureActivity)
setResult(
RESULT_OK,
Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId),
)
enabled = false
finish()
}
}
binding.renderedTemplate.text =
templateText?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY) }
binding.addButton.isEnabled = enabled && isValidServerId()
}
}
}
Loading
Loading