Skip to content

Commit

Permalink
Update TemplateWidget to use template subscriptions
Browse files Browse the repository at this point in the history
 - Instead of depending on entity state changes and refreshing any time there is a change, use the render_template subscription for the template widget to limit the amount of data and power used. To make this possible without too much abstractions, the TemplateWidget no longer implements BaseWidgetProvider.
 - Fix hardcoded "Loading" string
  • Loading branch information
jpelgrom committed Aug 17, 2022
1 parent 6fa9055 commit 4bcf375
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package io.homeassistant.companion.android.widgets.template

import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.os.Bundle
import android.text.Html.fromHtml
Expand All @@ -17,52 +19,184 @@ import androidx.core.graphics.toColorInt
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
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.util.getAttribute
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR

@AndroidEntryPoint
class TemplateWidget : BaseWidgetProvider() {
class TemplateWidget : AppWidgetProvider() {
companion object {
private const val TAG = "TemplateWidget"

const val UPDATE_VIEW =
"io.homeassistant.companion.android.widgets.template.TemplateWidget.UPDATE_VIEW"
const val RECEIVE_DATA =
"io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA"

internal const val EXTRA_TEMPLATE = "extra_template"
internal const val EXTRA_TEXT_SIZE = "EXTRA_TEXT_SIZE"
internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE"
internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR"

private var isSubscribed = false
private var widgetScope: CoroutineScope? = null
private val widgetTemplates = mutableMapOf<Int, String>()
private val widgetJobs = mutableMapOf<Int, Job>()
}

@Inject
lateinit var integrationUseCase: IntegrationRepository

@Inject
lateinit var templateWidgetDao: TemplateWidgetDao

private var thisSetScope = false
private var lastIntent = ""

init {
setupWidgetScope()
}

private fun setupWidgetScope() {
if (widgetScope == null || !widgetScope!!.isActive) {
widgetScope = CoroutineScope(Dispatchers.Main + Job())
thisSetScope = true
}
}

override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
widgetScope?.launch {
val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
// When the user deletes the widget, delete the preference associated with it.
mainScope.launch {
widgetScope?.launch {
templateWidgetDao.deleteAll(appWidgetIds)
appWidgetIds.forEach {
widgetTemplates.remove(it)
widgetJobs[it]?.cancel()
widgetJobs.remove(it)
}
}
}

override fun onReceive(context: Context, intent: Intent) {
lastIntent = intent.action.toString()
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)

super.onReceive(context, intent)
when (lastIntent) {
UPDATE_VIEW -> updateView(context, appWidgetId)
RECEIVE_DATA -> {
saveEntityConfiguration(
context,
intent.extras,
appWidgetId
)
onScreenOn(context)
}
Intent.ACTION_SCREEN_ON -> onScreenOn(context)
Intent.ACTION_SCREEN_OFF -> onScreenOff()
}
}

override fun isSubscribed(): Boolean = isSubscribed
private fun onScreenOn(context: Context) {
setupWidgetScope()
widgetScope!!.launch {
if (!integrationUseCase.isRegistered()) {
return@launch
}
updateAllWidgets(context)

val allWidgets = templateWidgetDao.getAll()
val widgetsWithDifferentTemplate = allWidgets.filter { it.template != widgetTemplates[it.id] }
if (widgetsWithDifferentTemplate.isNotEmpty()) {
if (thisSetScope) {
context.applicationContext.registerReceiver(
this@TemplateWidget,
IntentFilter(Intent.ACTION_SCREEN_OFF)
)
}

widgetsWithDifferentTemplate.forEach { widget ->
widgetJobs[widget.id]?.cancel()

val templateUpdates = integrationUseCase.getTemplateUpdates(widget.template)
if (templateUpdates != null) {
widgetTemplates[widget.id] = widget.template
widgetJobs[widget.id] = widgetScope!!.launch {
templateUpdates.collect {
onTemplateChanged(context, widget.id, it)
}
}
} else { // Remove data to make it retry on the next update
widgetTemplates.remove(widget.id)
widgetJobs.remove(widget.id)
}
}
}
}
}

override fun setSubscribed(subscribed: Boolean) {
isSubscribed = subscribed
private fun onScreenOff() {
if (thisSetScope) {
widgetScope?.cancel()
thisSetScope = false
widgetTemplates.clear()
widgetJobs.clear()
}
}

override fun getWidgetProvider(context: Context): ComponentName =
ComponentName(context, TemplateWidget::class.java)
private suspend fun updateAllWidgets(
context: Context
) {
val systemWidgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(ComponentName(context, TemplateWidget::class.java))
.toSet()
val dbWidgetIds = templateWidgetDao.getAll().map { it.id }

val invalidWidgetIds = dbWidgetIds.minus(systemWidgetIds)
if (invalidWidgetIds.isNotEmpty()) {
Log.i(TAG, "Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted")
onDeleted(context, invalidWidgetIds.toIntArray())
}

dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach {
updateView(context, it)
}
}

override suspend fun getAllWidgetIds(context: Context): List<Int> {
return templateWidgetDao.getAll().map { it.id }
private fun updateView(
context: Context,
appWidgetId: Int,
appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
) {
widgetScope?.launch {
val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity<Map<String, Any>>?): RemoteViews {
private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedTemplate: String? = null): RemoteViews {
// Every time AppWidgetManager.updateAppWidget(...) is called, the button listener
// and label need to be re-assigned, or the next time the layout updates
// (e.g home screen rotation) the widget will fall back on its default layout
Expand Down Expand Up @@ -97,9 +231,9 @@ class TemplateWidget : BaseWidgetProvider() {
}

// Content
var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: "Loading"
var renderedTemplate: String? = templateWidgetDao.get(appWidgetId)?.lastUpdate ?: context.getString(commonR.string.loading)
try {
renderedTemplate = integrationUseCase.renderTemplate(widget.template, mapOf()).toString()
renderedTemplate = suggestedTemplate ?: integrationUseCase.renderTemplate(widget.template, mapOf()).toString()
templateWidgetDao.updateTemplateWidgetLastUpdate(
appWidgetId,
renderedTemplate
Expand All @@ -124,7 +258,7 @@ class TemplateWidget : BaseWidgetProvider() {
}
}

override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) {
private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) {
if (extras == null) return

val template: String? = extras.getString(EXTRA_TEMPLATE)
Expand All @@ -137,7 +271,7 @@ class TemplateWidget : BaseWidgetProvider() {
return
}

mainScope.launch {
widgetScope?.launch {
templateWidgetDao.add(
TemplateWidgetEntity(
appWidgetId,
Expand All @@ -152,13 +286,10 @@ class TemplateWidget : BaseWidgetProvider() {
}
}

override suspend fun onEntityStateChanged(context: Context, entity: Entity<*>) {
getAllWidgetIds(context).forEach { appWidgetId ->
val intent = Intent(context, TemplateWidget::class.java).apply {
action = UPDATE_VIEW
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}
context.sendBroadcast(intent)
private fun onTemplateChanged(context: Context, appWidgetId: Int, template: String) {
widgetScope?.launch {
val views = getWidgetRemoteViews(context, appWidgetId, template)
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import io.homeassistant.companion.android.databinding.WidgetTemplateConfigureBin
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
import io.homeassistant.companion.android.util.getHexForColor
import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -170,7 +169,7 @@ class TemplateWidgetConfigureActivity : BaseWidgetConfigureActivity() {
}

val createIntent = Intent().apply {
action = BaseWidgetProvider.RECEIVE_DATA
action = TemplateWidget.RECEIVE_DATA
component = ComponentName(applicationContext, TemplateWidget::class.java)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
putExtra(TemplateWidget.EXTRA_TEMPLATE, binding.templateText.text.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface IntegrationRepository {
suspend fun getNotificationRateLimits(): RateLimitResponse

suspend fun renderTemplate(template: String, variables: Map<String, String>): String?
suspend fun getTemplateUpdates(template: String): Flow<String>?

suspend fun updateLocation(updateLocation: UpdateLocation)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ class IntegrationRepositoryImpl @Inject constructor(
else throw IntegrationException("Error calling integration request render_template")
}

override suspend fun getTemplateUpdates(template: String): Flow<String>? {
return webSocketRepository.getTemplateUpdates(template)
?.map {
it.result
}
}

override suspend fun updateLocation(updateLocation: UpdateLocation) {
val updateLocationRequest = createUpdateLocation(updateLocation)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.En
import io.homeassistant.companion.android.common.data.websocket.impl.entities.EntityRegistryUpdatedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.GetConfigResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
import kotlinx.coroutines.flow.Flow

interface WebSocketRepository {
Expand All @@ -25,6 +26,7 @@ interface WebSocketRepository {
suspend fun getAreaRegistryUpdates(): Flow<AreaRegistryUpdatedEvent>?
suspend fun getDeviceRegistryUpdates(): Flow<DeviceRegistryUpdatedEvent>?
suspend fun getEntityRegistryUpdates(): Flow<EntityRegistryUpdatedEvent>?
suspend fun getTemplateUpdates(template: String): Flow<TemplateUpdatedEvent>?
suspend fun getNotifications(): Flow<Map<String, Any>>?
suspend fun ackNotification(confirmId: String): Boolean
}
Loading

0 comments on commit 4bcf375

Please sign in to comment.