Skip to content

Commit

Permalink
Add 'toggle' tap action to entity state widget (#3798)
Browse files Browse the repository at this point in the history
* [WIP] Widget tap action: toggle entity

* Add feedback on press and failure

* Share code for pressing on entities

* Align cover press action

 - Toggle will stop if possible when opening/closing if supported so prefer toggle instead of open/close

* Toggle by default if supported

 - Set the default tap action for supported entities to toggle instead of refresh

* Update widget description
  • Loading branch information
jpelgrom authored Aug 19, 2023
1 parent e3ce9ed commit 7f14582
Show file tree
Hide file tree
Showing 16 changed files with 1,210 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.EntityExt
import io.homeassistant.companion.android.common.data.integration.getIcon
import io.homeassistant.companion.android.common.data.integration.onEntityPressedWithoutState
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.qs.TileDao
import io.homeassistant.companion.android.database.qs.TileEntity
Expand Down Expand Up @@ -236,22 +238,9 @@ abstract class TileExtensions : TileService() {
}
withContext(Dispatchers.IO) {
try {
serverManager.integrationRepository(tileData.serverId).callService(
tileData.entityId.split(".")[0],
when (tileData.entityId.split(".")[0]) {
"button", "input_button" -> "press"
in toggleDomains -> "toggle"
"lock" -> {
val state = serverManager.integrationRepository(tileData.serverId).getEntity(tileData.entityId)
if (state?.state == "locked") {
"unlock"
} else {
"lock"
}
}
else -> "turn_on"
},
hashMapOf("entity_id" to tileData.entityId)
onEntityPressedWithoutState(
tileData.entityId,
serverManager.integrationRepository(tileData.serverId)
)
Log.d(TAG, "Service call sent for tile ID: $tileId")
} catch (e: Exception) {
Expand Down Expand Up @@ -342,11 +331,7 @@ abstract class TileExtensions : TileService() {

companion object {
private const val TAG = "TileExtensions"
private val toggleDomains = listOf(
"automation", "cover", "fan", "humidifier", "input_boolean", "light",
"media_player", "remote", "siren", "switch"
)
private val toggleDomainsWithLock = toggleDomains.plus("lock")
private val toggleDomainsWithLock = EntityExt.DOMAINS_TOGGLE
private val validActiveStates = listOf("on", "open", "locked")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.fragment.app.viewModels
import com.google.accompanist.themeadapter.material.MdcTheme
import com.mikepenz.iconics.typeface.IIcon
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.EntityExt
import io.homeassistant.companion.android.settings.addHelpMenuProvider
import io.homeassistant.companion.android.settings.qs.views.ManageTilesView
import io.homeassistant.companion.android.util.icondialog.IconDialog
Expand All @@ -25,10 +26,7 @@ class ManageTilesFragment : Fragment() {

companion object {
private const val TAG = "TileFragment"
val validDomains = listOf(
"automation", "button", "cover", "fan", "humidifier", "input_boolean", "input_button", "light",
"lock", "media_player", "remote", "siren", "scene", "script", "switch"
)
val validDomains = EntityExt.APP_PRESS_ACTION_DOMAINS
}

val viewModel: ManageTilesViewModel by viewModels()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.util.Log
import android.util.TypedValue
import android.view.View
import android.widget.RemoteViews
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import com.google.android.material.color.DynamicColors
Expand All @@ -20,9 +21,11 @@ import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.canSupportPrecision
import io.homeassistant.companion.android.common.data.integration.friendlyState
import io.homeassistant.companion.android.common.data.integration.onEntityPressedWithoutState
import io.homeassistant.companion.android.database.widget.StaticWidgetDao
import io.homeassistant.companion.android.database.widget.StaticWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.database.widget.WidgetTapAction
import io.homeassistant.companion.android.util.getAttribute
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
import kotlinx.coroutines.launch
Expand All @@ -34,6 +37,8 @@ class EntityWidget : BaseWidgetProvider() {

companion object {
private const val TAG = "StaticWidget"
internal const val TOGGLE_ENTITY =
"io.homeassistant.companion.android.widgets.entity.EntityWidget.TOGGLE_ENTITY"

internal const val EXTRA_SERVER_ID = "EXTRA_SERVER_ID"
internal const val EXTRA_ENTITY_ID = "EXTRA_ENTITY_ID"
Expand All @@ -42,6 +47,7 @@ class EntityWidget : BaseWidgetProvider() {
internal const val EXTRA_TEXT_SIZE = "EXTRA_TEXT_SIZE"
internal const val EXTRA_STATE_SEPARATOR = "EXTRA_STATE_SEPARATOR"
internal const val EXTRA_ATTRIBUTE_SEPARATOR = "EXTRA_ATTRIBUTE_SEPARATOR"
internal const val EXTRA_TAP_ACTION = "EXTRA_TAP_ACTION"
internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE"
internal const val EXTRA_TEXT_COLOR = "EXTRA_TEXT_COLOR"

Expand All @@ -55,12 +61,13 @@ class EntityWidget : BaseWidgetProvider() {
ComponentName(context, EntityWidget::class.java)

override suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity<Map<String, Any>>?): RemoteViews {
val widget = staticWidgetDao.get(appWidgetId)

val intent = Intent(context, EntityWidget::class.java).apply {
action = UPDATE_VIEW
action = if (widget?.tapAction == WidgetTapAction.TOGGLE) TOGGLE_ENTITY else UPDATE_VIEW
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}

val widget = staticWidgetDao.get(appWidgetId)
val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable()
val views = RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_static_wrapper_dynamiccolor else R.layout.widget_static_wrapper_default).apply {
if (widget != null) {
Expand All @@ -83,6 +90,14 @@ class EntityWidget : BaseWidgetProvider() {
}

// Content
setViewVisibility(
R.id.widgetTextLayout,
View.VISIBLE
)
setViewVisibility(
R.id.widgetProgressBar,
View.INVISIBLE
)
val resolvedText = resolveTextToShow(
context,
serverId,
Expand Down Expand Up @@ -194,6 +209,12 @@ class EntityWidget : BaseWidgetProvider() {
val textSizeSelection: String? = extras.getString(EXTRA_TEXT_SIZE)
val stateSeparatorSelection: String? = extras.getString(EXTRA_STATE_SEPARATOR)
val attributeSeparatorSelection: String? = extras.getString(EXTRA_ATTRIBUTE_SEPARATOR)
val tapActionSelection: WidgetTapAction = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
extras.getSerializable(EXTRA_TAP_ACTION, WidgetTapAction::class.java)
} else {
@Suppress("DEPRECATION")
extras.getSerializable(EXTRA_TAP_ACTION) as? WidgetTapAction
} ?: WidgetTapAction.REFRESH
val backgroundTypeSelection: WidgetBackgroundType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
extras.getSerializable(EXTRA_BACKGROUND_TYPE, WidgetBackgroundType::class.java)
} else {
Expand Down Expand Up @@ -224,6 +245,7 @@ class EntityWidget : BaseWidgetProvider() {
textSizeSelection?.toFloatOrNull() ?: 30F,
stateSeparatorSelection ?: "",
attributeSeparatorSelection ?: "",
tapActionSelection,
staticWidgetDao.get(appWidgetId)?.lastUpdate ?: "",
backgroundTypeSelection,
textColorSelection
Expand All @@ -241,6 +263,45 @@ class EntityWidget : BaseWidgetProvider() {
}
}

private fun toggleEntity(context: Context, appWidgetId: Int) {
widgetScope?.launch {
// Show progress bar as feedback
val appWidgetManager = AppWidgetManager.getInstance(context)
val loadingViews = RemoteViews(context.packageName, R.layout.widget_static)
loadingViews.setViewVisibility(R.id.widgetProgressBar, View.VISIBLE)
loadingViews.setViewVisibility(R.id.widgetTextLayout, View.GONE)
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, loadingViews)

var success = false
staticWidgetDao.get(appWidgetId)?.let {
try {
onEntityPressedWithoutState(
it.entityId,
serverManager.integrationRepository(it.serverId)
)
success = true
} catch (e: Exception) {
Log.e(TAG, "Unable to send toggle service call", e)
}
}

if (!success) {
Toast.makeText(context, commonR.string.service_call_failure, Toast.LENGTH_LONG).show()

val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
} // else update will be triggered by websocket subscription
}
}

override fun onReceive(context: Context, intent: Intent) {
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
super.onReceive(context, intent)
when (lastIntent) {
TOGGLE_ENTITY -> toggleEntity(context, appWidgetId)
}
}

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
widgetScope?.launch {
staticWidgetDao.deleteAll(appWidgetIds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.color.DynamicColors
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.EntityExt
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.database.widget.StaticWidgetDao
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.database.widget.WidgetTapAction
import io.homeassistant.companion.android.databinding.WidgetStaticConfigureBinding
import io.homeassistant.companion.android.settings.widgets.ManageWidgetsViewModel
import io.homeassistant.companion.android.util.getHexForColor
Expand Down Expand Up @@ -122,6 +125,9 @@ class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() {

val staticWidget = staticWidgetDao.get(appWidgetId)

val tapActionValues = listOf(getString(commonR.string.widget_tap_action_toggle), getString(commonR.string.refresh))
binding.tapActionList.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, tapActionValues)

val backgroundTypeValues = mutableListOf(
getString(commonR.string.widget_background_type_daynight),
getString(commonR.string.widget_background_type_transparent)
Expand Down Expand Up @@ -162,6 +168,10 @@ class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() {
setupAttributes()
}

val toggleable = entity?.domain in EntityExt.APP_PRESS_ACTION_DOMAINS
binding.tapAction.isVisible = toggleable
binding.tapActionList.setSelection(if (toggleable && staticWidget.tapAction == WidgetTapAction.TOGGLE) 0 else 1)

binding.backgroundType.setSelection(
when {
staticWidget.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable() ->
Expand Down Expand Up @@ -272,6 +282,9 @@ class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() {
attributesAdapter.addAll(*fetchedAttributes?.keys.orEmpty().toTypedArray())
binding.widgetTextConfigAttribute.setTokenizer(CommaTokenizer())
runOnUiThread {
val toggleable = selectedEntity?.domain in EntityExt.APP_PRESS_ACTION_DOMAINS
binding.tapAction.isVisible = toggleable
binding.tapActionList.setSelection(if (toggleable) 0 else 1)
attributesAdapter.notifyDataSetChanged()
}
}
Expand Down Expand Up @@ -342,6 +355,14 @@ class EntityWidgetConfigureActivity : BaseWidgetConfigureActivity() {
)
}

intent.putExtra(
EntityWidget.EXTRA_TAP_ACTION,
when (binding.tapActionList.selectedItemPosition) {
0 -> WidgetTapAction.TOGGLE
else -> WidgetTapAction.REFRESH
}
)

intent.putExtra(
EntityWidget.EXTRA_BACKGROUND_TYPE,
when (binding.backgroundType.selectedItem as String?) {
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/layout/widget_static.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,14 @@
android:maxLines="2" />
</LinearLayout>
</LinearLayout>

<ProgressBar
android:id="@+id/widgetProgressBar"
android:layout_width="48dp"
android:layout_height="48dp"
android:visibility="invisible"
android:indeterminate="true"
android:indeterminateTint="@color/colorAccent"
android:layout_gravity="center"
/>
</FrameLayout>
22 changes: 22 additions & 0 deletions app/src/main/res/layout/widget_static_configure.xml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,28 @@
android:inputType="text" />
</LinearLayout>

<LinearLayout
android:id="@+id/tap_action"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="5dp"
android:text="@string/widget_tap_action_label" />

<Spinner
android:id="@+id/tap_action_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
Expand Down
Loading

0 comments on commit 7f14582

Please sign in to comment.