Skip to content

Commit 7b0edda

Browse files
committed
Add entity states to grid widget
1 parent c3eb2e3 commit 7b0edda

File tree

10 files changed

+234
-207
lines changed

10 files changed

+234
-207
lines changed

app/src/main/java/io/homeassistant/companion/android/HomeAssistantApplication.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import io.homeassistant.companion.android.util.LifecycleHandler
2626
import io.homeassistant.companion.android.websocket.WebsocketBroadcastReceiver
2727
import io.homeassistant.companion.android.widgets.button.ButtonWidget
2828
import io.homeassistant.companion.android.widgets.entity.EntityWidget
29+
import io.homeassistant.companion.android.widgets.grid.GridWidget
2930
import io.homeassistant.companion.android.widgets.mediaplayer.MediaPlayerControlsWidget
3031
import io.homeassistant.companion.android.widgets.template.TemplateWidget
3132
import javax.inject.Inject
@@ -287,6 +288,7 @@ open class HomeAssistantApplication : Application() {
287288
// Update widgets when the screen turns on, updates are skipped if widgets were not added
288289
val buttonWidget = ButtonWidget()
289290
val entityWidget = EntityWidget()
291+
val gridWidget = GridWidget()
290292
val mediaPlayerWidget = MediaPlayerControlsWidget()
291293
val templateWidget = TemplateWidget()
292294

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

297299
ContextCompat.registerReceiver(this, buttonWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
298300
ContextCompat.registerReceiver(this, entityWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
301+
ContextCompat.registerReceiver(this, gridWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
299302
ContextCompat.registerReceiver(this, mediaPlayerWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
300303
ContextCompat.registerReceiver(this, templateWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
301304
}
Lines changed: 175 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,45 @@
11
package io.homeassistant.companion.android.widgets.grid
22

3+
import android.app.PendingIntent
34
import android.appwidget.AppWidgetManager
4-
import android.appwidget.AppWidgetProvider
5+
import android.content.ComponentName
56
import android.content.Context
67
import android.content.Intent
8+
import android.os.Build
79
import android.os.Bundle
810
import android.util.Log
9-
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
10-
import com.fasterxml.jackson.module.kotlin.readValue
11+
import android.view.View
12+
import android.widget.RemoteViews
13+
import androidx.core.graphics.drawable.DrawableCompat
14+
import androidx.core.graphics.drawable.toBitmap
15+
import androidx.core.os.BundleCompat
16+
import androidx.core.widget.RemoteViewsCompat
17+
import com.mikepenz.iconics.IconicsDrawable
18+
import com.mikepenz.iconics.IconicsSize
19+
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
20+
import com.mikepenz.iconics.utils.padding
21+
import com.mikepenz.iconics.utils.size
1122
import dagger.hilt.android.AndroidEntryPoint
12-
import io.homeassistant.companion.android.common.data.servers.ServerManager
23+
import io.homeassistant.companion.android.R
24+
import io.homeassistant.companion.android.common.R as commonR
25+
import io.homeassistant.companion.android.common.data.integration.Entity
26+
import io.homeassistant.companion.android.common.data.integration.onEntityPressedWithoutState
1327
import io.homeassistant.companion.android.database.widget.GridWidgetDao
28+
import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
29+
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
1430
import io.homeassistant.companion.android.widgets.common.WidgetAuthenticationActivity
15-
import java.util.regex.Pattern
31+
import io.homeassistant.companion.android.widgets.grid.config.GridConfiguration
32+
import io.homeassistant.companion.android.widgets.grid.config.GridItem
1633
import javax.inject.Inject
17-
import kotlin.text.split
34+
import kotlin.String
35+
import kotlin.collections.Map
1836
import kotlinx.coroutines.CoroutineScope
1937
import kotlinx.coroutines.Dispatchers
2038
import kotlinx.coroutines.Job
2139
import kotlinx.coroutines.launch
2240

2341
@AndroidEntryPoint
24-
class GridWidget : AppWidgetProvider() {
42+
class GridWidget : BaseWidgetProvider() {
2543
companion object {
2644
private const val TAG = "GridWidget"
2745
const val CALL_SERVICE =
@@ -30,11 +48,10 @@ class GridWidget : AppWidgetProvider() {
3048
"io.homeassistant.companion.android.widgets.grid.GridWidget.CALL_SERVICE_AUTH"
3149
const val EXTRA_ACTION_ID =
3250
"io.homeassistant.companion.android.widgets.grid.GridWidget.EXTRA_ACTION_ID"
51+
const val EXTRA_CONFIG =
52+
"io.homeassistant.companion.android.widgets.grid.GridWidget.EXTRA_CONFIG"
3353
}
3454

35-
@Inject
36-
lateinit var serverManager: ServerManager
37-
3855
@Inject
3956
lateinit var gridWidgetDao: GridWidgetDao
4057

@@ -52,37 +69,13 @@ class GridWidget : AppWidgetProvider() {
5269
}
5370
}
5471

55-
override fun onUpdate(
56-
context: Context,
57-
appWidgetManager: AppWidgetManager,
58-
appWidgetIds: IntArray
59-
) {
60-
appWidgetIds.forEach { appWidgetId ->
61-
val gridConfig = gridWidgetDao.get(appWidgetId)?.asGridConfiguration()
62-
appWidgetManager.updateAppWidget(appWidgetId, gridConfig.asRemoteViews(context, appWidgetId))
72+
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
73+
widgetScope?.launch {
74+
gridWidgetDao.deleteAll(appWidgetIds)
75+
appWidgetIds.forEach { removeSubscription(it) }
6376
}
6477
}
6578

66-
override fun onAppWidgetOptionsChanged(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetId: Int, newOptions: Bundle?) {
67-
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
68-
}
69-
70-
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
71-
super.onDeleted(context, appWidgetIds)
72-
}
73-
74-
override fun onEnabled(context: Context?) {
75-
super.onEnabled(context)
76-
}
77-
78-
override fun onDisabled(context: Context?) {
79-
super.onDisabled(context)
80-
}
81-
82-
override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) {
83-
super.onRestored(context, oldWidgetIds, newWidgetIds)
84-
}
85-
8679
private fun authThenCallConfiguredAction(context: Context, appWidgetId: Int, actionId: Int) {
8780
Log.d(TAG, "Calling authentication, then configured action")
8881

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

108101
mainScope.launch {
109-
// Load the action call data from Shared Preferences
110-
val domain = item?.domain
111-
val action = item?.service
112-
val actionDataJson = item?.serviceData
113-
114-
Log.d(
115-
TAG,
116-
"Action Call Data loaded:" + System.lineSeparator() +
117-
"domain: " + domain + System.lineSeparator() +
118-
"action: " + action + System.lineSeparator() +
119-
"action_data: " + actionDataJson
120-
)
102+
val entityId = item?.entityId
121103

122-
if (domain == null || action == null || actionDataJson == null) {
123-
Log.w(TAG, "Action Call Data incomplete. Aborting action call")
104+
Log.d(TAG, "Action Call Data loaded: entity_id: $entityId")
105+
if (entityId == null) {
106+
Log.w(TAG, "Action Call Data incomplete. Aborting action call")
124107
} else {
125-
// If everything loaded correctly, package the action data and attempt the call
108+
// If everything loaded correctly, attempt the call
126109
try {
127-
// Convert JSON to HashMap
128-
val actionDataMap: HashMap<String, Any> =
129-
jacksonObjectMapper().readValue(actionDataJson)
130-
131-
if (actionDataMap["entity_id"] != null) {
132-
val entityIdWithoutBrackets = Pattern.compile("\\[(.*?)\\]")
133-
.matcher(actionDataMap["entity_id"].toString())
134-
if (entityIdWithoutBrackets.find()) {
135-
val value = entityIdWithoutBrackets.group(1)
136-
if (value != null) {
137-
if (value == "all" ||
138-
value.split(",").contains("all")
139-
) {
140-
actionDataMap["entity_id"] = "all"
141-
}
142-
}
143-
}
144-
}
145-
146110
Log.d(TAG, "Sending action call to Home Assistant")
147-
serverManager.integrationRepository(widget.gridWidget.serverId).callAction(domain, action, actionDataMap)
111+
onEntityPressedWithoutState(
112+
entityId,
113+
serverManager.integrationRepository(widget.gridWidget.serverId)
114+
)
148115
Log.d(TAG, "Action call sent successfully")
149116
} catch (e: Exception) {
150117
Log.e(TAG, "Failed to call action", e)
151118
}
152119
}
153120
}
154121
}
122+
123+
override fun getWidgetProvider(context: Context): ComponentName =
124+
ComponentName(context, GridWidget::class.java)
125+
126+
override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity<Map<String, Any>>?): RemoteViews {
127+
val gridConfig = gridWidgetDao.get(appWidgetId)?.asGridConfiguration()
128+
val entityStates = gridConfig?.let { getEntityStates(gridConfig.serverId ?: 0, gridConfig.items.map { it.entityId }, suggestedEntity) }
129+
return gridConfig.asRemoteViews(context, appWidgetId, entityStates)
130+
}
131+
132+
override suspend fun getAllWidgetIdsWithEntities(context: Context): Map<Int, Pair<Int, List<String>>> =
133+
gridWidgetDao.getAll().associate {
134+
val entityIds = it.items
135+
.map { it.entityId }
136+
.filterNot { it.isEmpty() }
137+
138+
it.gridWidget.id to (it.gridWidget.serverId to entityIds)
139+
}
140+
141+
override fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) {
142+
val extras = extras ?: return
143+
val config = BundleCompat.getParcelable(extras, EXTRA_CONFIG, GridConfiguration::class.java) ?: return
144+
145+
widgetScope?.launch {
146+
gridWidgetDao.add(config.asDbEntity(appWidgetId))
147+
}
148+
149+
onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId))
150+
}
151+
152+
override suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>) {
153+
widgetScope?.launch {
154+
val views = getWidgetRemoteViews(context, appWidgetId, entity as Entity<Map<String, Any>>)
155+
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views)
156+
}
157+
}
158+
159+
private fun GridConfiguration?.asRemoteViews(context: Context, widgetId: Int, entityStates: Map<String, String>? = null): RemoteViews {
160+
val layout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
161+
R.layout.widget_grid_wrapper_dynamiccolor
162+
} else {
163+
R.layout.widget_grid_wrapper_default
164+
}
165+
val remoteViews = RemoteViews(context.packageName, layout)
166+
167+
if (this != null) {
168+
remoteViews.apply {
169+
if (label.isNullOrEmpty()) {
170+
setViewVisibility(R.id.widgetLabel, View.GONE)
171+
} else {
172+
setViewVisibility(R.id.widgetLabel, View.VISIBLE)
173+
setTextViewText(R.id.widgetLabel, label)
174+
}
175+
176+
val intent = Intent(context, GridWidget::class.java).apply {
177+
action = if (requireAuthentication) CALL_SERVICE_AUTH else CALL_SERVICE
178+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
179+
}
180+
setPendingIntentTemplate(
181+
R.id.widgetGrid,
182+
PendingIntent.getBroadcast(
183+
context,
184+
widgetId,
185+
intent,
186+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
187+
)
188+
)
189+
190+
RemoteViewsCompat.setRemoteAdapter(
191+
context = context,
192+
remoteViews = this,
193+
appWidgetId = widgetId,
194+
viewId = R.id.widgetGrid,
195+
items = items.asRemoteCollection(context, entityStates)
196+
)
197+
}
198+
}
199+
return remoteViews
200+
}
201+
202+
private fun List<GridItem>.asRemoteCollection(context: Context, entityStates: Map<String, String>? = null) =
203+
RemoteViewsCompat.RemoteCollectionItems.Builder().apply {
204+
setHasStableIds(true)
205+
forEach { action ->
206+
addItem(
207+
context = context,
208+
item = action,
209+
state = entityStates?.get(action.entityId)
210+
)
211+
}
212+
}.build()
213+
214+
private suspend fun getEntityStates(serverId: Int, entities: List<String>, suggestedEntity: Entity<Map<String, Any>>? = null): Map<String, String> =
215+
entities.associateWith {
216+
if (suggestedEntity?.entityId != it) {
217+
serverManager.integrationRepository(serverId).getEntity(it)?.state ?: "Unknown"
218+
} else {
219+
suggestedEntity.state
220+
}
221+
}
222+
223+
private fun RemoteViewsCompat.RemoteCollectionItems.Builder.addItem(context: Context, item: GridItem, state: String? = null) {
224+
addItem(item.id.toLong(), item.asRemoteViews(context, state))
225+
}
226+
227+
private fun GridItem.asRemoteViews(context: Context, state: String? = null) =
228+
RemoteViews(context.packageName, R.layout.widget_grid_button).apply {
229+
val icon = CommunityMaterial.getIconByMdiName(icon)
230+
icon?.let {
231+
val iconDrawable = DrawableCompat.wrap(
232+
IconicsDrawable(context, icon).apply {
233+
padding = IconicsSize.dp(2)
234+
size = IconicsSize.dp(24)
235+
}
236+
)
237+
238+
setImageViewBitmap(R.id.widgetImageButton, iconDrawable.toBitmap())
239+
}
240+
setTextViewText(
241+
R.id.widgetLabel,
242+
label
243+
)
244+
setTextViewText(
245+
R.id.widgetState,
246+
state ?: context.getString(commonR.string.widget_grid_entity_state_unknown)
247+
)
248+
249+
val fillInIntent = Intent().apply {
250+
Bundle().also { extras ->
251+
extras.putInt(EXTRA_ACTION_ID, id)
252+
putExtras(extras)
253+
}
254+
}
255+
setOnClickFillInIntent(R.id.gridButtonLayout, fillInIntent)
256+
}
155257
}

0 commit comments

Comments
 (0)