Skip to content

Commit

Permalink
Add entity states to grid widget
Browse files Browse the repository at this point in the history
  • Loading branch information
mrdanielps committed Nov 11, 2024
1 parent c3eb2e3 commit 7b0edda
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import io.homeassistant.companion.android.util.LifecycleHandler
import io.homeassistant.companion.android.websocket.WebsocketBroadcastReceiver
import io.homeassistant.companion.android.widgets.button.ButtonWidget
import io.homeassistant.companion.android.widgets.entity.EntityWidget
import io.homeassistant.companion.android.widgets.grid.GridWidget
import io.homeassistant.companion.android.widgets.mediaplayer.MediaPlayerControlsWidget
import io.homeassistant.companion.android.widgets.template.TemplateWidget
import javax.inject.Inject
Expand Down Expand Up @@ -287,6 +288,7 @@ open class HomeAssistantApplication : Application() {
// Update widgets when the screen turns on, updates are skipped if widgets were not added
val buttonWidget = ButtonWidget()
val entityWidget = EntityWidget()
val gridWidget = GridWidget()
val mediaPlayerWidget = MediaPlayerControlsWidget()
val templateWidget = TemplateWidget()

Expand All @@ -296,6 +298,7 @@ open class HomeAssistantApplication : Application() {

ContextCompat.registerReceiver(this, buttonWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, entityWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, gridWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, mediaPlayerWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, templateWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
package io.homeassistant.companion.android.widgets.grid

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.os.Build
import android.os.Bundle
import android.util.Log
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import android.view.View
import android.widget.RemoteViews
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.os.BundleCompat
import androidx.core.widget.RemoteViewsCompat
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.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.onEntityPressedWithoutState
import io.homeassistant.companion.android.database.widget.GridWidgetDao
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity
import java.util.regex.Pattern
import io.homeassistant.companion.android.widgets.grid.config.GridConfiguration
import io.homeassistant.companion.android.widgets.grid.config.GridItem
import javax.inject.Inject
import kotlin.text.split
import kotlin.String
import kotlin.collections.Map
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

@AndroidEntryPoint
class GridWidget : AppWidgetProvider() {
class GridWidget : BaseWidgetProvider() {
companion object {
private const val TAG = "GridWidget"
const val CALL_SERVICE =
Expand All @@ -30,11 +48,10 @@ class GridWidget : AppWidgetProvider() {
"io.homeassistant.companion.android.widgets.grid.GridWidget.CALL_SERVICE_AUTH"
const val EXTRA_ACTION_ID =
"io.homeassistant.companion.android.widgets.grid.GridWidget.EXTRA_ACTION_ID"
const val EXTRA_CONFIG =
"io.homeassistant.companion.android.widgets.grid.GridWidget.EXTRA_CONFIG"
}

@Inject
lateinit var serverManager: ServerManager

@Inject
lateinit var gridWidgetDao: GridWidgetDao

Expand All @@ -52,37 +69,13 @@ class GridWidget : AppWidgetProvider() {
}
}

override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId ->
val gridConfig = gridWidgetDao.get(appWidgetId)?.asGridConfiguration()
appWidgetManager.updateAppWidget(appWidgetId, gridConfig.asRemoteViews(context, appWidgetId))
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
widgetScope?.launch {
gridWidgetDao.deleteAll(appWidgetIds)
appWidgetIds.forEach { removeSubscription(it) }
}
}

override fun onAppWidgetOptionsChanged(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetId: Int, newOptions: Bundle?) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
}

override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds)
}

override fun onEnabled(context: Context?) {
super.onEnabled(context)
}

override fun onDisabled(context: Context?) {
super.onDisabled(context)
}

override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) {
super.onRestored(context, oldWidgetIds, newWidgetIds)
}

private fun authThenCallConfiguredAction(context: Context, appWidgetId: Int, actionId: Int) {
Log.d(TAG, "Calling authentication, then configured action")

Expand All @@ -106,50 +99,159 @@ class GridWidget : AppWidgetProvider() {
val item = widget?.items?.find { it.id == actionId }

mainScope.launch {
// Load the action call data from Shared Preferences
val domain = item?.domain
val action = item?.service
val actionDataJson = item?.serviceData

Log.d(
TAG,
"Action Call Data loaded:" + System.lineSeparator() +
"domain: " + domain + System.lineSeparator() +
"action: " + action + System.lineSeparator() +
"action_data: " + actionDataJson
)
val entityId = item?.entityId

if (domain == null || action == null || actionDataJson == null) {
Log.w(TAG, "Action Call Data incomplete. Aborting action call")
Log.d(TAG, "Action Call Data loaded: entity_id: $entityId")
if (entityId == null) {
Log.w(TAG, "Action Call Data incomplete. Aborting action call")
} else {
// If everything loaded correctly, package the action data and attempt the call
// If everything loaded correctly, attempt the call
try {
// Convert JSON to HashMap
val actionDataMap: HashMap<String, Any> =
jacksonObjectMapper().readValue(actionDataJson)

if (actionDataMap["entity_id"] != null) {
val entityIdWithoutBrackets = Pattern.compile("\\[(.*?)\\]")
.matcher(actionDataMap["entity_id"].toString())
if (entityIdWithoutBrackets.find()) {
val value = entityIdWithoutBrackets.group(1)
if (value != null) {
if (value == "all" ||
value.split(",").contains("all")
) {
actionDataMap["entity_id"] = "all"
}
}
}
}

Log.d(TAG, "Sending action call to Home Assistant")
serverManager.integrationRepository(widget.gridWidget.serverId).callAction(domain, action, actionDataMap)
onEntityPressedWithoutState(
entityId,
serverManager.integrationRepository(widget.gridWidget.serverId)
)
Log.d(TAG, "Action call sent successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to call action", e)
}
}
}
}

override fun getWidgetProvider(context: Context): ComponentName =
ComponentName(context, GridWidget::class.java)

override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity<Map<String, Any>>?): RemoteViews {
val gridConfig = gridWidgetDao.get(appWidgetId)?.asGridConfiguration()
val entityStates = gridConfig?.let { getEntityStates(gridConfig.serverId ?: 0, gridConfig.items.map { it.entityId }, suggestedEntity) }
return gridConfig.asRemoteViews(context, appWidgetId, entityStates)
}

override suspend fun getAllWidgetIdsWithEntities(context: Context): Map<Int, Pair<Int, List<String>>> =
gridWidgetDao.getAll().associate {
val entityIds = it.items
.map { it.entityId }
.filterNot { it.isEmpty() }

it.gridWidget.id to (it.gridWidget.serverId to entityIds)
}

override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) {
val extras = extras ?: return
val config = BundleCompat.getParcelable(extras, EXTRA_CONFIG, GridConfiguration::class.java) ?: return

widgetScope?.launch {
gridWidgetDao.add(config.asDbEntity(appWidgetId))
}

onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId))
}

override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) {
widgetScope?.launch {
val views = getWidgetRemoteViews(context, appWidgetId, entity as Entity<Map<String, Any>>)
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views)
}
}

private fun GridConfiguration?.asRemoteViews(context: Context, widgetId: Int, entityStates: Map<String, String>? = null): RemoteViews {
val layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
R.layout.widget_grid_wrapper_dynamiccolor
} else {
R.layout.widget_grid_wrapper_default
}
val remoteViews = RemoteViews(context.packageName, layout)

if (this != null) {
remoteViews.apply {
if (label.isNullOrEmpty()) {
setViewVisibility(R.id.widgetLabel, View.GONE)
} else {
setViewVisibility(R.id.widgetLabel, View.VISIBLE)
setTextViewText(R.id.widgetLabel, label)
}

val intent = Intent(context, GridWidget::class.java).apply {
action = if (requireAuthentication) CALL_SERVICE_AUTH else CALL_SERVICE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
}
setPendingIntentTemplate(
R.id.widgetGrid,
PendingIntent.getBroadcast(
context,
widgetId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
)

RemoteViewsCompat.setRemoteAdapter(
context = context,
remoteViews = this,
appWidgetId = widgetId,
viewId = R.id.widgetGrid,
items = items.asRemoteCollection(context, entityStates)
)
}
}
return remoteViews
}

private fun List<GridItem>.asRemoteCollection(context: Context, entityStates: Map<String, String>? = null) =
RemoteViewsCompat.RemoteCollectionItems.Builder().apply {
setHasStableIds(true)
forEach { action ->
addItem(
context = context,
item = action,
state = entityStates?.get(action.entityId)
)
}
}.build()

private suspend fun getEntityStates(serverId: Int, entities: List<String>, suggestedEntity: Entity<Map<String, Any>>? = null): Map<String, String> =
entities.associateWith {
if (suggestedEntity?.entityId != it) {
serverManager.integrationRepository(serverId).getEntity(it)?.state ?: "Unknown"
} else {
suggestedEntity.state
}
}

private fun RemoteViewsCompat.RemoteCollectionItems.Builder.addItem(context: Context, item: GridItem, state: String? = null) {
addItem(item.id.toLong(), item.asRemoteViews(context, state))
}

private fun GridItem.asRemoteViews(context: Context, state: String? = null) =
RemoteViews(context.packageName, R.layout.widget_grid_button).apply {
val icon = CommunityMaterial.getIconByMdiName(icon)
icon?.let {
val iconDrawable = DrawableCompat.wrap(
IconicsDrawable(context, icon).apply {
padding = IconicsSize.dp(2)
size = IconicsSize.dp(24)
}
)

setImageViewBitmap(R.id.widgetImageButton, iconDrawable.toBitmap())
}
setTextViewText(
R.id.widgetLabel,
label
)
setTextViewText(
R.id.widgetState,
state ?: context.getString(commonR.string.widget_grid_entity_state_unknown)
)

val fillInIntent = Intent().apply {
Bundle().also { extras ->
extras.putInt(EXTRA_ACTION_ID, id)
putExtras(extras)
}
}
setOnClickFillInIntent(R.id.gridButtonLayout, fillInIntent)
}
}
Loading

0 comments on commit 7b0edda

Please sign in to comment.