Multiple media players per widget (#2534)

* Update Media Player Widget Preview

* Add support for multiple media players. optionally show media source label

* fix crop of media player widget preview image. avoid unnecessary api call's when finding active media player. add db migrations. add text color to media source label. set default value of showsource to false.

* remove placeholder source for source icon to fix widget preview
This commit is contained in:
Jannis Göing 2022-05-29 14:57:52 +02:00 committed by GitHub
parent 7b9fb83f1e
commit b9c1092302
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 834 additions and 79 deletions

View file

@ -28,7 +28,9 @@ import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWid
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.widgets.BaseWidgetProvider
import kotlinx.coroutines.launch
import java.util.LinkedList
import javax.inject.Inject
import kotlin.collections.HashMap
import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
@ -58,6 +60,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
internal const val EXTRA_ENTITY_ID = "EXTRA_ENTITY_ID"
internal const val EXTRA_LABEL = "EXTRA_LABEL"
internal const val EXTRA_SHOW_VOLUME = "EXTRA_SHOW_VOLUME"
internal const val EXTRA_SHOW_SOURCE = "EXTRA_SHOW_VOLUME_SOURCE"
internal const val EXTRA_SHOW_SKIP = "EXTRA_INCLUDE_SKIP"
internal const val EXTRA_SHOW_SEEK = "EXTRA_INCLUDE_SEEK"
internal const val EXTRA_BACKGROUND_TYPE = "EXTRA_BACKGROUND_TYPE"
@ -96,6 +99,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
mainScope.launch {
val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
onScreenOn(context)
}
}
@ -144,12 +148,15 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val useDynamicColors = widget?.backgroundType == WidgetBackgroundType.DYNAMICCOLOR && DynamicColors.isDynamicColorAvailable()
return RemoteViews(context.packageName, if (useDynamicColors) R.layout.widget_media_controls_wrapper_dynamiccolor else R.layout.widget_media_controls_wrapper_default).apply {
if (widget != null) {
val entityId: String = widget.entityId
val entityIds: LinkedList<String> = LinkedList()
entityIds.addAll(widget.entityId.split(","))
var label: String? = widget.label
val showVolume: Boolean = widget.showVolume
val showSkip: Boolean = widget.showSkip
val showSeek: Boolean = widget.showSeek
val entity = getEntity(context, widget.entityId, suggestedEntity)
val showSource: Boolean = widget.showSource
val entity = getEntity(context, entityIds, suggestedEntity)
if (entity?.state.equals("playing")) {
setImageViewResource(
@ -166,7 +173,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val artist = (entity?.attributes?.get("media_artist") ?: entity?.attributes?.get("media_album_artist"))?.toString()
val title = entity?.attributes?.get("media_title")?.toString()
val album = entity?.attributes?.get("media_album_name")?.toString()
val icon = entity?.attributes?.get("icon")?.toString()
var icon = entity?.attributes?.get("icon")?.toString()
if ((artist != null || album != null) && title != null) {
setTextViewText(
@ -197,9 +204,15 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
if (artist != null) {
label = artist
}
if (artist == null && title != null) {
label = title
}
if (artist == null && title == null && album != null) {
label = album
}
setTextViewText(
R.id.widgetLabel,
label ?: entityId
label ?: entity?.entityId
)
setViewVisibility(
R.id.widgetMediaInfoTitle,
@ -215,24 +228,17 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
)
}
if (icon != null && icon.startsWith("mdi")) {
val iconName = icon.split(":")[1]
val iconDrawable: Bitmap = IconicsDrawable(context, "cmd-$iconName").toBitmap()
setImageViewBitmap(
R.id.widgetSourceIcon,
iconDrawable
)
setViewVisibility(
R.id.widgetSourceIcon,
View.VISIBLE
)
} else {
setViewVisibility(
R.id.widgetSourceIcon,
View.INVISIBLE
)
if (icon == null || !icon.startsWith("mdi") || !icon.contains(":")) {
icon = "mdi:cast"
}
val iconName = icon.split(":")[1]
val iconDrawable: Bitmap = IconicsDrawable(context, "cmd-$iconName").toBitmap()
setImageViewBitmap(
R.id.widgetSourceIcon,
iconDrawable
)
val entityPictureUrl = entity?.attributes?.get("entity_picture")?.toString()
val baseUrl = urlUseCase.getUrl().toString().removeSuffix("/")
val url = if (entityPictureUrl?.startsWith("http") == true) entityPictureUrl else "$baseUrl$entityPictureUrl"
@ -382,6 +388,13 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
setViewVisibility(R.id.widgetRewindButton, View.GONE)
setViewVisibility(R.id.widgetFastForwardButton, View.GONE)
}
if (showSource) {
setTextViewText(R.id.widgetSourceLabel, entity?.attributes?.get("friendly_name").toString())
setViewVisibility(R.id.widgetSourceLabel, View.VISIBLE)
} else {
setViewVisibility(R.id.widgetSourceLabel, View.INVISIBLE)
}
} else {
setTextViewText(R.id.widgetLabel, "")
}
@ -392,13 +405,19 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
return AppDatabase.getInstance(context).mediaPlayCtrlWidgetDao().getAll().map { it.id }
}
private suspend fun getEntity(context: Context, entityId: String, suggestedEntity: Entity<Map<String, Any>>?): Entity<Map<String, Any>>? {
private suspend fun getEntity(context: Context, entityIds: List<String>, suggestedEntity: Entity<Map<String, Any>>?): Entity<Map<String, Any>>? {
val entity: Entity<Map<String, Any>>?
try {
entity = if (suggestedEntity != null && suggestedEntity.entityId == entityId) {
entity = if (suggestedEntity != null && entityIds.contains(suggestedEntity.entityId)) {
suggestedEntity
} else {
entityId.let { integrationUseCase.getEntity(it) }
val entities: LinkedList<Entity<Map<String, Any>>?> = LinkedList()
entityIds.forEach {
val e = integrationUseCase.getEntity(it)
if (e?.state == "playing") return e
entities.add(e)
}
return entities[0]
}
} catch (e: Exception) {
Log.d(TAG, "Failed to fetch entity or entity does not exist")
@ -434,13 +453,13 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
super.onScreenOn(context)
}
UPDATE_MEDIA_IMAGE -> updateView(context, appWidgetId)
CALL_PREV_TRACK -> callPreviousTrackService(appWidgetId)
CALL_PREV_TRACK -> callPreviousTrackService(context, appWidgetId)
CALL_REWIND -> callRewindService(context, appWidgetId)
CALL_PLAYPAUSE -> callPlayPauseService(appWidgetId)
CALL_PLAYPAUSE -> callPlayPauseService(context, appWidgetId)
CALL_FASTFORWARD -> callFastForwardService(context, appWidgetId)
CALL_NEXT_TRACK -> callNextTrackService(appWidgetId)
CALL_VOLUME_DOWN -> callVolumeDownService(appWidgetId)
CALL_VOLUME_UP -> callVolumeUpService(appWidgetId)
CALL_NEXT_TRACK -> callNextTrackService(context, appWidgetId)
CALL_VOLUME_DOWN -> callVolumeDownService(context, appWidgetId)
CALL_VOLUME_UP -> callVolumeUpService(context, appWidgetId)
}
}
@ -452,9 +471,10 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val showSkip: Boolean? = extras.getBoolean(EXTRA_SHOW_SKIP)
val showSeek: Boolean? = extras.getBoolean(EXTRA_SHOW_SEEK)
val showVolume: Boolean? = extras.getBoolean(EXTRA_SHOW_VOLUME)
val showSource: Boolean? = extras.getBoolean(EXTRA_SHOW_SOURCE)
val backgroundType: WidgetBackgroundType = extras.getSerializable(EXTRA_BACKGROUND_TYPE) as WidgetBackgroundType
if (entitySelection == null || showSkip == null || showSeek == null || showVolume == null) {
if (entitySelection == null || showSkip == null || showSeek == null || showVolume == null || showSource == null) {
Log.e(TAG, "Did not receive complete configuration data")
return
}
@ -473,6 +493,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
showSkip,
showSeek,
showVolume,
showSource,
backgroundType
)
)
@ -483,16 +504,17 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
override suspend fun onEntityStateChanged(context: Context, entity: Entity<*>) {
AppDatabase.getInstance(context).mediaPlayCtrlWidgetDao().getAll().forEach {
if (it.entityId == entity.entityId) {
val entityIds = it.entityId.split(",")
if (entityIds.contains(entity.entityId)) {
mainScope.launch {
val views = getWidgetRemoteViews(context, it.id, entity as Entity<Map<String, Any>>)
val views = getWidgetRemoteViews(context, it.id, getEntity(context, entityIds, null))
AppWidgetManager.getInstance(context).updateAppWidget(it.id, views)
}
}
}
}
private fun callPreviousTrackService(appWidgetId: Int) {
private fun callPreviousTrackService(context: Context, appWidgetId: Int) {
mainScope.launch {
Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId")
val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId)
@ -510,8 +532,9 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "media_previous_track"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entity.entityId)
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entityId)
integrationUseCase.callService(domain, service, serviceDataMap)
}
@ -551,9 +574,10 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "media_seek"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf(
"entity_id" to entity.entityId,
"entity_id" to entityId,
"seek_position" to currentTime - 10
)
@ -561,7 +585,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
}
}
private fun callPlayPauseService(appWidgetId: Int) {
private fun callPlayPauseService(context: Context, appWidgetId: Int) {
mainScope.launch {
Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId")
val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId)
@ -579,8 +603,9 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "media_play_pause"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entity.entityId)
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entityId)
integrationUseCase.callService(domain, service, serviceDataMap)
}
@ -620,9 +645,10 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "media_seek"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf(
"entity_id" to entity.entityId,
"entity_id" to entityId,
"seek_position" to currentTime + 10
)
@ -630,7 +656,7 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
}
}
private fun callNextTrackService(appWidgetId: Int) {
private fun callNextTrackService(context: Context, appWidgetId: Int) {
mainScope.launch {
Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId")
val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId)
@ -648,14 +674,15 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "media_next_track"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entity.entityId)
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entityId)
integrationUseCase.callService(domain, service, serviceDataMap)
}
}
private fun callVolumeDownService(appWidgetId: Int) {
private fun callVolumeDownService(context: Context, appWidgetId: Int) {
mainScope.launch {
Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId")
val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId)
@ -673,14 +700,15 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "volume_down"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entity.entityId)
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entityId)
integrationUseCase.callService(domain, service, serviceDataMap)
}
}
private fun callVolumeUpService(appWidgetId: Int) {
private fun callVolumeUpService(context: Context, appWidgetId: Int) {
mainScope.launch {
Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId")
val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId)
@ -698,8 +726,9 @@ class MediaPlayerControlsWidget : BaseWidgetProvider() {
val domain = "media_player"
val service = "volume_up"
val entityId: String = getEntity(context, entity.entityId.split(","), null)?.entityId.toString()
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entity.entityId)
val serviceDataMap: HashMap<String, Any> = hashMapOf("entity_id" to entityId)
integrationUseCase.callService(domain, service, serviceDataMap)
}

View file

@ -8,9 +8,9 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.MultiAutoCompleteTextView
import android.widget.Toast
import androidx.core.content.getSystemService
import androidx.lifecycle.lifecycleScope
@ -28,7 +28,9 @@ import io.homeassistant.companion.android.widgets.BaseWidgetConfigureActivity
import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.LinkedList
import javax.inject.Inject
import kotlin.collections.LinkedHashMap
import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
@ -50,7 +52,7 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
private lateinit var binding: WidgetMediaControlsConfigureBinding
private var entities = LinkedHashMap<String, Entity<Any>>()
private var selectedEntity: Entity<Any>? = null
private var selectedEntities: LinkedList<Entity<*>?> = LinkedList()
public override fun onCreate(icicle: Bundle?) {
super.onCreate(icicle)
@ -64,7 +66,7 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
binding.addButton.setOnClickListener {
if (requestLauncherSetup) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && selectedEntity != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && selectedEntities.size > 0) {
getSystemService<AppWidgetManager>()?.requestPinAppWidget(
ComponentName(this, MediaPlayerControlsWidget::class.java),
null,
@ -120,9 +122,10 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
binding.widgetShowVolumeButtonCheckbox.isChecked = mediaPlayerWidget.showVolume
binding.widgetShowSeekButtonsCheckbox.isChecked = mediaPlayerWidget.showSeek
binding.widgetShowSkipButtonsCheckbox.isChecked = mediaPlayerWidget.showSkip
val entity = runBlocking {
binding.widgetShowMediaPlayerSource.isChecked = mediaPlayerWidget.showSource
val entities = runBlocking {
try {
integrationUseCase.getEntity(mediaPlayerWidget.entityId)
mediaPlayerWidget.entityId.split(",").map { s -> integrationUseCase.getEntity(s.trim()) }
} catch (e: Exception) {
Log.e(TAG, "Unable to get entity information", e)
Toast.makeText(applicationContext, commonR.string.widget_entity_fetch_error, Toast.LENGTH_LONG)
@ -138,8 +141,8 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
backgroundTypeValues.indexOf(getString(commonR.string.widget_background_type_daynight))
}
)
if (entity != null)
selectedEntity = entity as Entity<Any>?
if (entities != null)
selectedEntities.addAll(entities)
binding.addButton.setText(commonR.string.update_widget)
binding.deleteButton.visibility = View.VISIBLE
binding.deleteButton.setOnClickListener(onDeleteWidget)
@ -147,8 +150,8 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
val entityAdapter = SingleItemArrayAdapter<Entity<Any>>(this) { it?.entityId ?: "" }
binding.widgetTextConfigEntityId.setAdapter(entityAdapter)
binding.widgetTextConfigEntityId.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer())
binding.widgetTextConfigEntityId.onFocusChangeListener = dropDownOnFocus
binding.widgetTextConfigEntityId.onItemClickListener = entityDropDownOnItemClick
lifecycleScope.launch {
try {
@ -179,11 +182,6 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
}
}
private val entityDropDownOnItemClick =
AdapterView.OnItemClickListener { parent, _, position, _ ->
selectedEntity = parent.getItemAtPosition(position) as Entity<Any>?
}
private fun onAddWidget() {
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
showAddWidgetError()
@ -199,9 +197,16 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
selectedEntities = LinkedList()
val se = binding.widgetTextConfigEntityId.text.split(",")
se.forEach {
val e = entities[it.trim()]
if (e != null) selectedEntities.add(e)
}
intent.putExtra(
MediaPlayerControlsWidget.EXTRA_ENTITY_ID,
selectedEntity!!.entityId
selectedEntities.map { e -> e?.entityId }.reduce { a, b -> "$a,$b" }
)
intent.putExtra(
MediaPlayerControlsWidget.EXTRA_LABEL,
@ -219,6 +224,10 @@ class MediaPlayerControlsWidgetConfigureActivity : BaseWidgetConfigureActivity()
MediaPlayerControlsWidget.EXTRA_SHOW_SEEK,
binding.widgetShowSeekButtonsCheckbox.isChecked
)
intent.putExtra(
MediaPlayerControlsWidget.EXTRA_SHOW_SOURCE,
binding.widgetShowMediaPlayerSource.isChecked
)
intent.putExtra(
MediaPlayerControlsWidget.EXTRA_BACKGROUND_TYPE,
when (binding.backgroundType.selectedItem as String?) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -57,58 +57,82 @@
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:padding="8dp"
android:tint="?colorWidgetOnBackground" />
<TextView
android:id="@+id/widgetSourceLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:ellipsize="marquee"
android:visibility="invisible"
android:tint="?colorWidgetOnBackground"
android:src="@drawable/app_icon_round" />
android:textColor="?colorWidgetOnBackground"
android:layout_toStartOf="@id/widgetSourceIcon"
tools:text="Media player Source" />
<LinearLayout
android:id="@+id/widgetMediaInfolayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_above="@id/widgetMediaButtonlayout"
android:layout_margin="4dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="0dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
android:padding="0dp"
android:paddingStart="4dp"
android:paddingEnd="4dp">
<TextView
android:id="@+id/widgetLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:maxLines="2"
android:minLines="1"
android:singleLine="true"
android:ellipsize="marquee"
android:padding="0dp"
android:text="@string/widget_label_placeholder_text_media_player"
android:textAlignment="center"
android:textColor="?colorWidgetOnBackground"
android:textSize="24sp" />
android:textSize="20sp" />
<TextView
android:id="@+id/widgetMediaInfoTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:maxLines="1"
android:minLines="1"
android:visibility="gone"
android:singleLine="true"
android:ellipsize="marquee"
android:padding="0dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAlignment="textStart"
android:textColor="?colorWidgetOnBackground"
android:textSize="18sp" />
android:textSize="16sp"
android:visibility="gone" />
<TextView
android:id="@+id/widgetMediaInfoArtist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:maxLines="1"
android:minLines="1"
android:visibility="gone"
android:singleLine="true"
android:ellipsize="marquee"
android:padding="0dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAlignment="textStart"
android:textColor="?colorWidgetOnBackground"
android:textSize="14sp" />
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
<LinearLayout

View file

@ -28,9 +28,9 @@
android:layout_height="wrap_content"
android:labelFor="@id/widget_text_config_entity_id"
android:padding="5dp"
android:text="@string/label_entity_id" />
android:text="@string/label_entity_ids" />
<AutoCompleteTextView
<MultiAutoCompleteTextView
android:id="@+id/widget_text_config_entity_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -69,6 +69,16 @@
android:checked="true"
android:text="@string/widget_media_show_seek" />
<CheckBox
android:id="@+id/widget_show_media_player_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layoutDirection="rtl"
android:layout_margin="5dp"
android:checked="true"
android:text="@string/widget_media_show_source" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"

View file

@ -0,0 +1,678 @@
{
"formatVersion": 1,
"database": {
"version": 27,
"identityHash": "3761a46a6cfa6a15c9d8ed4935f0beb7",
"entities": [
{
"tableName": "sensor_attributes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
"fields": [
{
"fieldPath": "sensorId",
"columnName": "sensor_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "value_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sensor_id",
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Authentication_List",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `Username` TEXT NOT NULL, `Password` TEXT NOT NULL, PRIMARY KEY(`host`))",
"fields": [
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "Username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "Password",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"host"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sensors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER NOT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "registered",
"columnName": "registered",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSentState",
"columnName": "last_sent_state",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "stateType",
"columnName": "state_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "deviceClass",
"columnName": "device_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "unitOfMeasurement",
"columnName": "unit_of_measurement",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "stateClass",
"columnName": "state_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "entityCategory",
"columnName": "entity_category",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coreRegistration",
"columnName": "core_registration",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appRegistration",
"columnName": "app_registration",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sensor_settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))",
"fields": [
{
"fieldPath": "sensorId",
"columnName": "sensor_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "value_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entries",
"columnName": "entries",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sensor_id",
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "button_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `icon_id` INTEGER NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "iconId",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "service",
"columnName": "service",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serviceData",
"columnName": "service_data",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "backgroundType",
"columnName": "background_type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'DAYNIGHT'"
},
{
"fieldPath": "textColor",
"columnName": "text_color",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "camera_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "mediaplayctrls_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, `label` TEXT, `showSkip` INTEGER NOT NULL, `showSeek` INTEGER NOT NULL, `showVolume` INTEGER NOT NULL, `showSource` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "showSkip",
"columnName": "showSkip",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showSeek",
"columnName": "showSeek",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showVolume",
"columnName": "showVolume",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showSource",
"columnName": "showSource",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
},
{
"fieldPath": "backgroundType",
"columnName": "background_type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'DAYNIGHT'"
},
{
"fieldPath": "textColor",
"columnName": "text_color",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "static_widget",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entity_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "attributeIds",
"columnName": "attribute_ids",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "textSize",
"columnName": "text_size",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "stateSeparator",
"columnName": "state_separator",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "attributeSeparator",
"columnName": "attribute_separator",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUpdate",
"columnName": "last_update",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "backgroundType",
"columnName": "background_type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'DAYNIGHT'"
},
{
"fieldPath": "textColor",
"columnName": "text_color",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "template_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "template",
"columnName": "template",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "textSize",
"columnName": "text_size",
"affinity": "REAL",
"notNull": true,
"defaultValue": "12.0"
},
{
"fieldPath": "lastUpdate",
"columnName": "last_update",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "backgroundType",
"columnName": "background_type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'DAYNIGHT'"
},
{
"fieldPath": "textColor",
"columnName": "text_color",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "notification_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "qs_tiles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tileId` TEXT NOT NULL, `icon_id` INTEGER, `entityId` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tileId",
"columnName": "tileId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconId",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subtitle",
"columnName": "subtitle",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocketSetting` TEXT NOT NULL, `sensorUpdateFrequency` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "websocketSetting",
"columnName": "websocketSetting",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sensorUpdateFrequency",
"columnName": "sensorUpdateFrequency",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3761a46a6cfa6a15c9d8ed4935f0beb7')"
]
}
}

View file

@ -70,10 +70,11 @@ import io.homeassistant.companion.android.common.R as commonR
Favorites::class,
Setting::class
],
version = 26,
version = 27,
autoMigrations = [
AutoMigration(from = 24, to = 25),
AutoMigration(from = 25, to = 26)
AutoMigration(from = 25, to = 26),
AutoMigration(from = 26, to = 27)
]
)
@TypeConverters(

View file

@ -18,6 +18,8 @@ data class MediaPlayerControlsWidgetEntity(
val showSeek: Boolean,
@ColumnInfo(name = "showVolume")
val showVolume: Boolean,
@ColumnInfo(name = "showSource", defaultValue = "false")
val showSource: Boolean,
@ColumnInfo(name = "background_type", defaultValue = "DAYNIGHT")
override val backgroundType: WidgetBackgroundType = WidgetBackgroundType.DAYNIGHT,
@ColumnInfo(name = "text_color")

View file

@ -235,6 +235,7 @@
<string name="label_attribute">Attribute:</string>
<string name="label_dynamic_data">Data:</string>
<string name="label_entity_id">Entity ID:</string>
<string name="label_entity_ids">Entity ID(s):</string>
<string name="label_icon">Icon:</string>
<string name="label_label">Label:</string>
<string name="label_service">Service:</string>
@ -762,6 +763,7 @@
<string name="widget_media_show_volume">Show Volume Buttons</string>
<string name="widget_media_show_seek">Show Seek Buttons</string>
<string name="widget_media_show_skip">Show Skip Buttons</string>
<string name="widget_media_show_source">Show Source Label</string>
<string name="widget_separator_input_hint">No separator</string>
<string name="widget_separator_label">State and attribute separator:</string>
<string name="widget_spinner_icon">Icon:</string>