diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4c4a2decc..5d33c421b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -133,6 +133,7 @@ dependencies {
implementation(Config.Dependency.Misc.jackson)
implementation(Config.Dependency.Square.okhttp)
+ implementation(Config.Dependency.Square.picasso)
"fullImplementation"(Config.Dependency.Play.location)
"fullImplementation"(Config.Dependency.Firebase.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index db099f60d..92c856b56 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -78,6 +78,23 @@
android:resource="@xml/entity_widget_info" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -107,7 +124,11 @@
-
+
+
+
+
+
diff --git a/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt b/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt
index 11a5387f4..22c58c647 100644
--- a/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt
+++ b/app/src/main/java/io/homeassistant/companion/android/database/AppDatabase.kt
@@ -25,6 +25,8 @@ import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.database.sensor.Setting
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity
+import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao
+import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity
import io.homeassistant.companion.android.database.widget.StaticWidgetDao
import io.homeassistant.companion.android.database.widget.StaticWidgetEntity
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
@@ -38,15 +40,17 @@ import kotlinx.coroutines.runBlocking
Sensor::class,
Setting::class,
ButtonWidgetEntity::class,
+ MediaPlayerControlsWidgetEntity::class,
StaticWidgetEntity::class,
TemplateWidgetEntity::class
],
- version = 11
+ version = 12
)
abstract class AppDatabase : RoomDatabase() {
abstract fun authenticationDao(): AuthenticationDao
abstract fun sensorDao(): SensorDao
abstract fun buttonWidgetDao(): ButtonWidgetDao
+ abstract fun mediaPlayCtrlWidgetDao(): MediaPlayerControlsWidgetDao
abstract fun staticWidgetDao(): StaticWidgetDao
abstract fun templateWidgetDao(): TemplateWidgetDao
@@ -83,7 +87,8 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_7_8,
MIGRATION_8_9,
MIGRATION_9_10,
- MIGRATION_10_11
+ MIGRATION_10_11,
+ MIGRATION_11_12
)
.fallbackToDestructiveMigration()
.build()
@@ -254,6 +259,12 @@ abstract class AppDatabase : RoomDatabase() {
}
}
+ private val MIGRATION_11_12 = object : Migration(11, 12) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `mediaplayctrls_widgets` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, `label` TEXT, `showSkip` INTEGER NOT NULL, `showSeek` INTEGER NOT NULL, PRIMARY KEY(`id`))")
+ }
+ }
+
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
diff --git a/app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetDao.kt b/app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetDao.kt
new file mode 100644
index 000000000..51b926295
--- /dev/null
+++ b/app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetDao.kt
@@ -0,0 +1,23 @@
+package io.homeassistant.companion.android.database.widget
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Update
+
+@Dao
+interface MediaPlayerControlsWidgetDao {
+
+ @Query("SELECT * FROM mediaplayctrls_widgets WHERE id = :id")
+ fun get(id: Int): MediaPlayerControlsWidgetEntity?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun add(mediaPlayCtrlWidgetEntity: MediaPlayerControlsWidgetEntity)
+
+ @Update
+ fun update(mediaPlayCtrlWidgetEntity: MediaPlayerControlsWidgetEntity)
+
+ @Query("DELETE FROM mediaplayctrls_widgets WHERE id = :id")
+ fun delete(id: Int)
+}
diff --git a/app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt b/app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt
new file mode 100644
index 000000000..a803edf93
--- /dev/null
+++ b/app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt
@@ -0,0 +1,19 @@
+package io.homeassistant.companion.android.database.widget
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "mediaplayctrls_widgets")
+data class MediaPlayerControlsWidgetEntity(
+ @PrimaryKey
+ val id: Int,
+ @ColumnInfo(name = "entityId")
+ val entityId: String,
+ @ColumnInfo(name = "label")
+ val label: String?,
+ @ColumnInfo(name = "showSkip")
+ val showSkip: Boolean,
+ @ColumnInfo(name = "showSeek")
+ val showSeek: Boolean
+)
diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/ProviderComponent.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/ProviderComponent.kt
index 6edd191bd..c299860c0 100644
--- a/app/src/main/java/io/homeassistant/companion/android/widgets/ProviderComponent.kt
+++ b/app/src/main/java/io/homeassistant/companion/android/widgets/ProviderComponent.kt
@@ -6,6 +6,8 @@ import io.homeassistant.companion.android.widgets.button.ButtonWidget
import io.homeassistant.companion.android.widgets.button.ButtonWidgetConfigureActivity
import io.homeassistant.companion.android.widgets.entity.EntityWidget
import io.homeassistant.companion.android.widgets.entity.EntityWidgetConfigureActivity
+import io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget
+import io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidgetConfigureActivity
import io.homeassistant.companion.android.widgets.template.TemplateWidget
import io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigureActivity
@@ -13,14 +15,14 @@ import io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigu
interface ProviderComponent {
fun inject(receiver: ButtonWidget)
-
fun inject(activity: ButtonWidgetConfigureActivity)
- fun inject(receiver: EntityWidget)
+ fun inject(receiver: MediaPlayerControlsWidget)
+ fun inject(activity: MediaPlayerControlsWidgetConfigureActivity)
+ fun inject(receiver: EntityWidget)
fun inject(activity: EntityWidgetConfigureActivity)
fun inject(receiver: TemplateWidget)
-
fun inject(activity: TemplateWidgetConfigureActivity)
}
diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidget.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidget.kt
new file mode 100644
index 000000000..36645da03
--- /dev/null
+++ b/app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidget.kt
@@ -0,0 +1,501 @@
+package io.homeassistant.companion.android.widgets.media_player_controls
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.util.Log
+import android.view.View
+import android.widget.RemoteViews
+import android.widget.Toast
+import com.squareup.picasso.Picasso
+import io.homeassistant.companion.android.R
+import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
+import io.homeassistant.companion.android.common.data.integration.Entity
+import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
+import io.homeassistant.companion.android.database.AppDatabase
+import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao
+import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetEntity
+import io.homeassistant.companion.android.widgets.DaggerProviderComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+class MediaPlayerControlsWidget : AppWidgetProvider() {
+
+ companion object {
+ private const val TAG = "MediaPlayCtrlsWidget"
+ internal const val RECEIVE_DATA =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.RECEIVE_DATA"
+ internal const val UPDATE_MEDIA_IMAGE =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.UPDATE_MEDIA_IMAGE"
+ internal const val CALL_PREV_TRACK =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_PREV_TRACK"
+ internal const val CALL_REWIND =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_REWIND"
+ internal const val CALL_PLAYPAUSE =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_PLAYPAUSE"
+ internal const val CALL_FASTFORWARD =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_FASTFORWARD"
+ internal const val CALL_NEXT_TRACK =
+ "io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_NEXT_TRACK"
+
+ internal const val EXTRA_ENTITY_ID = "EXTRA_ENTITY_ID"
+ internal const val EXTRA_LABEL = "EXTRA_LABEL"
+ internal const val EXTRA_SHOW_SKIP = "EXTRA_INCLUDE_SKIP"
+ internal const val EXTRA_SHOW_SEEK = "EXTRA_INCLUDE_SEEK"
+ }
+
+ @Inject
+ lateinit var integrationUseCase: IntegrationRepository
+
+ lateinit var mediaPlayCtrlWidgetDao: MediaPlayerControlsWidgetDao
+
+ private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ mediaPlayCtrlWidgetDao = AppDatabase.getInstance(context).mediaPlayCtrlWidgetDao()
+ // There may be multiple widgets active, so update all of them
+ appWidgetIds.forEach { appWidgetId ->
+ updateAppWidget(
+ context,
+ appWidgetId,
+ appWidgetManager
+ )
+ }
+ }
+
+ private fun updateAppWidget(
+ context: Context,
+ appWidgetId: Int,
+ appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
+ ) {
+ mainScope.launch {
+ val views = getWidgetRemoteViews(context, appWidgetId)
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+ }
+
+ private suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int): RemoteViews {
+ val updateMediaIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply {
+ action = UPDATE_MEDIA_IMAGE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ val prevTrackIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply {
+ action = CALL_PREV_TRACK
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ val rewindIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply {
+ action = CALL_REWIND
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ val playPauseIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply {
+ action = CALL_PLAYPAUSE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ val fastForwardIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply {
+ action = CALL_FASTFORWARD
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ val nextTrackIntent = Intent(context, MediaPlayerControlsWidget::class.java).apply {
+ action = CALL_NEXT_TRACK
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ }
+
+ return RemoteViews(context.packageName, R.layout.widget_media_controls).apply {
+ val widget = mediaPlayCtrlWidgetDao.get(appWidgetId)
+ if (widget != null) {
+ val entityId: String = widget.entityId
+ val label: String? = widget.label
+ val showSkip: Boolean = widget.showSkip
+ val showSeek: Boolean = widget.showSeek
+
+ setTextViewText(
+ R.id.widgetLabel,
+ label ?: entityId
+ )
+ val entityPictureUrl = retrieveMediaPlayerImageUrl(context, entityId)
+ if (entityPictureUrl == null) {
+ setImageViewResource(
+ R.id.widgetMediaImage,
+ R.drawable.app_icon
+ )
+ setViewVisibility(
+ R.id.widgetMediaPlaceholder,
+ View.VISIBLE
+ )
+ setViewVisibility(
+ R.id.widgetMediaImage,
+ View.GONE
+ )
+ } else {
+ setViewVisibility(
+ R.id.widgetMediaImage,
+ View.VISIBLE
+ )
+ setViewVisibility(
+ R.id.widgetMediaPlaceholder,
+ View.GONE
+ )
+ Log.d(TAG, "Fetching media preview image")
+ Handler().post {
+ Picasso.get().load(entityPictureUrl).into(
+ this,
+ R.id.widgetMediaImage,
+ intArrayOf(appWidgetId)
+ )
+ Log.d(TAG, "Fetch and load complete")
+ }
+ }
+
+ setOnClickPendingIntent(
+ R.id.widgetMediaImage,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ updateMediaIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ setOnClickPendingIntent(
+ R.id.widgetMediaPlaceholder,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ updateMediaIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ setOnClickPendingIntent(
+ R.id.widgetPlayPauseButton,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ playPauseIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+
+ if (showSkip) {
+ setOnClickPendingIntent(
+ R.id.widgetPrevTrackButton,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ prevTrackIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ setOnClickPendingIntent(
+ R.id.widgetNextTrackButton,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ nextTrackIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ } else {
+ setViewVisibility(R.id.widgetPrevTrackButton, View.GONE)
+ setViewVisibility(R.id.widgetNextTrackButton, View.GONE)
+ }
+
+ if (showSeek) {
+ setOnClickPendingIntent(
+ R.id.widgetRewindButton,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ rewindIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ setOnClickPendingIntent(
+ R.id.widgetFastForwardButton,
+ PendingIntent.getBroadcast(
+ context,
+ appWidgetId,
+ fastForwardIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ } else {
+ setViewVisibility(R.id.widgetRewindButton, View.GONE)
+ setViewVisibility(R.id.widgetFastForwardButton, View.GONE)
+ }
+ }
+ }
+ }
+
+ private suspend fun retrieveMediaPlayerImageUrl(context: Context, entityId: String): String? {
+ val entity: Entity