11package io.homeassistant.companion.android.widgets.grid
22
3+ import android.app.PendingIntent
34import android.appwidget.AppWidgetManager
4- import android.appwidget.AppWidgetProvider
5+ import android.content.ComponentName
56import android.content.Context
67import android.content.Intent
8+ import android.os.Build
79import android.os.Bundle
810import 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
1122import 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
1327import io.homeassistant.companion.android.database.widget.GridWidgetDao
28+ import io.homeassistant.companion.android.util.icondialog.getIconByMdiName
29+ import io.homeassistant.companion.android.widgets.BaseWidgetProvider
1430import 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
1633import javax.inject.Inject
17- import kotlin.text.split
34+ import kotlin.String
35+ import kotlin.collections.Map
1836import kotlinx.coroutines.CoroutineScope
1937import kotlinx.coroutines.Dispatchers
2038import kotlinx.coroutines.Job
2139import 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