From 51072b9e0de95a086ab58ef681aaede01471ad49 Mon Sep 17 00:00:00 2001 From: "Kevin T. Berstene" Date: Tue, 13 Oct 2020 12:13:50 -0400 Subject: [PATCH] Created a media player control widget (#1005) * Added initial media player widget code. Has some work to do * ktLint Fixes and corrected TODOs * Finished TODO: Media image fetch and update * Corrected rewind/fastforward functionality so it actually seeks * Made requested minor changes * Additional minor requested changes * Appeasing the mighty ktlint * Added integration GET request for single entity instead of requesting all entities and parsing results * Corrected dark mode widget icons * Fix merge conflicts and migration. * Added toast notification for entity fetch failure Co-authored-by: Basset, Justin --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 23 +- .../companion/android/database/AppDatabase.kt | 15 +- .../widget/MediaPlayerControlsWidgetDao.kt | 23 + .../widget/MediaPlayerControlsWidgetEntity.kt | 19 + .../android/widgets/ProviderComponent.kt | 8 +- .../MediaPlayerControlsWidget.kt | 501 ++++++++++++++++++ ...iaPlayerControlsWidgetConfigureActivity.kt | 170 ++++++ app/src/main/res/drawable/ic_fastforward.xml | 8 + app/src/main/res/drawable/ic_next_track.xml | 8 + app/src/main/res/drawable/ic_playpause.xml | 8 + app/src/main/res/drawable/ic_prev_track.xml | 8 + app/src/main/res/drawable/ic_rewind.xml | 8 + .../widget_example_media_player_controls.png | Bin 0 -> 7300 bytes .../main/res/layout/widget_media_controls.xml | 130 +++++ .../widget_media_controls_configure.xml | 91 ++++ app/src/main/res/values/strings.xml | 11 +- .../xml/media_player_control_widget_info.xml | 10 + buildSrc/src/main/kotlin/Config.kt | 3 + 19 files changed, 1038 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetDao.kt create mode 100644 app/src/main/java/io/homeassistant/companion/android/database/widget/MediaPlayerControlsWidgetEntity.kt create mode 100644 app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidget.kt create mode 100644 app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidgetConfigureActivity.kt create mode 100644 app/src/main/res/drawable/ic_fastforward.xml create mode 100644 app/src/main/res/drawable/ic_next_track.xml create mode 100644 app/src/main/res/drawable/ic_playpause.xml create mode 100644 app/src/main/res/drawable/ic_prev_track.xml create mode 100644 app/src/main/res/drawable/ic_rewind.xml create mode 100644 app/src/main/res/drawable/widget_example_media_player_controls.png create mode 100644 app/src/main/res/layout/widget_media_controls.xml create mode 100644 app/src/main/res/layout/widget_media_controls_configure.xml create mode 100644 app/src/main/res/xml/media_player_control_widget_info.xml 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> + try { + entity = integrationUseCase.getEntity(entityId) + } catch (e: Exception) { + Log.d(TAG, "Failed to fetch entity or entity does not exist") + Toast.makeText(context, R.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() + return null + } + + return entity.attributes["entity_picture"]?.toString() + } + + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + + Log.d( + TAG, "Broadcast received: " + System.lineSeparator() + + "Broadcast action: " + action + System.lineSeparator() + + "AppWidgetId: " + appWidgetId + ) + + ensureInjected(context) + + mediaPlayCtrlWidgetDao = AppDatabase.getInstance(context).mediaPlayCtrlWidgetDao() + + super.onReceive(context, intent) + when (action) { + RECEIVE_DATA -> saveEntityConfiguration(context, intent.extras, appWidgetId) + UPDATE_MEDIA_IMAGE -> updateAppWidget(context, appWidgetId) + CALL_PREV_TRACK -> callPreviousTrackService(context, appWidgetId) + CALL_REWIND -> callRewindService(context, appWidgetId) + CALL_PLAYPAUSE -> callPlayPauseService(context, appWidgetId) + CALL_FASTFORWARD -> callFastForwardService(context, appWidgetId) + CALL_NEXT_TRACK -> callNextTrackService(context, appWidgetId) + } + } + + private fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int) { + if (extras == null) return + + val entitySelection: String? = extras.getString(EXTRA_ENTITY_ID) + val labelSelection: String? = extras.getString(EXTRA_LABEL) + val showSkip: Boolean? = extras.getBoolean(EXTRA_SHOW_SKIP) + val showSeek: Boolean? = extras.getBoolean(EXTRA_SHOW_SEEK) + + if (entitySelection == null || showSkip == null || showSeek == null) { + Log.e(TAG, "Did not receive complete configuration data") + return + } + + mainScope.launch { + Log.d( + TAG, "Saving service call config data:" + System.lineSeparator() + + "entity id: " + entitySelection + System.lineSeparator() + ) + mediaPlayCtrlWidgetDao.add( + MediaPlayerControlsWidgetEntity( + appWidgetId, + entitySelection, + labelSelection, + showSkip, + showSeek + ) + ) + + onUpdate(context, AppWidgetManager.getInstance(context), intArrayOf(appWidgetId)) + } + } + + 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) + + if (entity == null) { + Log.d(TAG, "Failed to retrieve media player entity") + return@launch + } + + Log.d( + TAG, "Calling previous track service:" + System.lineSeparator() + + "entity id: " + entity.entityId + System.lineSeparator() + ) + + val domain = "media_player" + val service = "media_previous_track" + + val serviceDataMap: HashMap = hashMapOf("entity_id" to entity.entityId) + + integrationUseCase.callService(domain, service, serviceDataMap) + } + } + + private fun callRewindService(context: Context, appWidgetId: Int) { + mainScope.launch { + Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") + val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + + if (entity == null) { + Log.d(TAG, "Failed to retrieve media player entity") + return@launch + } + + Log.d( + TAG, "Calling rewind service:" + System.lineSeparator() + + "entity id: " + entity.entityId + System.lineSeparator() + ) + + val currentEntityInfo: Entity> + try { + currentEntityInfo = integrationUseCase.getEntity(entity.entityId) + } catch (e: Exception) { + Log.d(TAG, "Failed to fetch entity or entity does not exist") + Toast.makeText(context, R.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() + return@launch + } + + val fetchedAttributes = currentEntityInfo.attributes + val currentTime = fetchedAttributes["media_position"]?.toString()?.toDoubleOrNull() + + if (currentTime == null) { + Log.d(TAG, "Failed to get entity current time, aborting call") + return@launch + } + + val domain = "media_player" + val service = "media_seek" + + val serviceDataMap: HashMap = hashMapOf( + "entity_id" to entity.entityId, + "seek_position" to currentTime - 10 + ) + + integrationUseCase.callService(domain, service, serviceDataMap) + } + } + + 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) + + if (entity == null) { + Log.d(TAG, "Failed to retrieve media player entity") + return@launch + } + + Log.d( + TAG, "Calling play/pause service:" + System.lineSeparator() + + "entity id: " + entity.entityId + System.lineSeparator() + ) + + val domain = "media_player" + val service = "media_play_pause" + + val serviceDataMap: HashMap = hashMapOf("entity_id" to entity.entityId) + + integrationUseCase.callService(domain, service, serviceDataMap) + } + } + + private fun callFastForwardService(context: Context, appWidgetId: Int) { + mainScope.launch { + Log.d(TAG, "Retrieving media player entity for app widget $appWidgetId") + val entity: MediaPlayerControlsWidgetEntity? = mediaPlayCtrlWidgetDao.get(appWidgetId) + + if (entity == null) { + Log.d(TAG, "Failed to retrieve media player entity") + return@launch + } + + Log.d( + TAG, "Calling fast forward service:" + System.lineSeparator() + + "entity id: " + entity.entityId + System.lineSeparator() + ) + + val currentEntityInfo: Entity> + try { + currentEntityInfo = integrationUseCase.getEntity(entity.entityId) + } catch (e: Exception) { + Log.d(TAG, "Failed to fetch entity or entity does not exist") + Toast.makeText(context, R.string.widget_entity_fetch_error, Toast.LENGTH_LONG).show() + return@launch + } + + val fetchedAttributes = currentEntityInfo.attributes + val currentTime = fetchedAttributes["media_position"]?.toString()?.toDoubleOrNull() + + if (currentTime == null) { + Log.d(TAG, "Failed to get entity current time, aborting call") + return@launch + } + + val domain = "media_player" + val service = "media_seek" + + val serviceDataMap: HashMap = hashMapOf( + "entity_id" to entity.entityId, + "seek_position" to currentTime + 10 + ) + + integrationUseCase.callService(domain, service, serviceDataMap) + } + } + + 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) + + if (entity == null) { + Log.d(TAG, "Failed to retrieve media player entity") + return@launch + } + + Log.d( + TAG, "Calling next track service:" + System.lineSeparator() + + "entity id: " + entity.entityId + System.lineSeparator() + ) + + val domain = "media_player" + val service = "media_next_track" + + val serviceDataMap: HashMap = hashMapOf("entity_id" to entity.entityId) + + integrationUseCase.callService(domain, service, serviceDataMap) + } + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + mediaPlayCtrlWidgetDao = AppDatabase.getInstance(context).mediaPlayCtrlWidgetDao() + // When the user deletes the widget, delete the preference associated with it. + for (appWidgetId in appWidgetIds) { + mediaPlayCtrlWidgetDao.delete(appWidgetId) + } + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } + + private fun ensureInjected(context: Context) { + if (context.applicationContext is GraphComponentAccessor) { + DaggerProviderComponent.builder() + .appComponent((context.applicationContext as GraphComponentAccessor).appComponent) + .build() + .inject(this) + } else { + throw Exception("Application Context passed is not of our application!") + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidgetConfigureActivity.kt b/app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidgetConfigureActivity.kt new file mode 100644 index 000000000..a21fa7049 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/widgets/media_player_controls/MediaPlayerControlsWidgetConfigureActivity.kt @@ -0,0 +1,170 @@ +package io.homeassistant.companion.android.widgets.media_player_controls + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.AutoCompleteTextView +import android.widget.Toast +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.widgets.DaggerProviderComponent +import io.homeassistant.companion.android.widgets.common.SingleItemArrayAdapter +import javax.inject.Inject +import kotlinx.android.synthetic.main.widget_media_controls_configure.add_button +import kotlinx.android.synthetic.main.widget_media_controls_configure.label +import kotlinx.android.synthetic.main.widget_media_controls_configure.widget_show_seek_buttons_checkbox +import kotlinx.android.synthetic.main.widget_media_controls_configure.widget_show_skip_buttons_checkbox +import kotlinx.android.synthetic.main.widget_media_controls_configure.widget_text_config_entity_id +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +class MediaPlayerControlsWidgetConfigureActivity : Activity() { + + companion object { + private const val TAG: String = "MediaWidgetConfigAct" + } + + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + @Inject + lateinit var integrationUseCase: IntegrationRepository + private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + + private var entities = LinkedHashMap>() + private var selectedEntity: Entity? = null + + public override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if the user presses the back button. + setResult(RESULT_CANCELED) + + setContentView(R.layout.widget_media_controls_configure) + + add_button.setOnClickListener(addWidgetButtonClickListener) + + // Find the widget id from the intent. + val intent = intent + val extras = intent.extras + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + + // If this activity was started with an intent without an app widget ID, finish with an error. + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + // Inject components + DaggerProviderComponent.builder() + .appComponent((application as GraphComponentAccessor).appComponent) + .build() + .inject(this) + + val entityAdapter = SingleItemArrayAdapter>(this) { it?.entityId ?: "" } + + widget_text_config_entity_id.setAdapter(entityAdapter) + widget_text_config_entity_id.onFocusChangeListener = dropDownOnFocus + widget_text_config_entity_id.onItemClickListener = entityDropDownOnItemClick + + mainScope.launch { + try { + // Fetch entities + val fetchedEntities = integrationUseCase.getEntities() + fetchedEntities.sortBy { e -> e.entityId } + fetchedEntities.forEach { + val entityId = it.entityId + val domain = entityId.split(".")[0] + + if (domain == "media_player") { + entities[entityId] = it + } + } + entityAdapter.addAll(entities.values) + entityAdapter.sort() + + runOnUiThread { + entityAdapter.notifyDataSetChanged() + } + } catch (e: Exception) { + // If entities fail to load, it's okay to pass + // an empty map to the dynamicFieldAdapter + Log.e(TAG, "Failed to query entities", e) + } + } + } + + private val dropDownOnFocus = View.OnFocusChangeListener { view, hasFocus -> + if (hasFocus && view is AutoCompleteTextView) { + view.showDropDown() + } + } + + private val entityDropDownOnItemClick = + AdapterView.OnItemClickListener { parent, _, position, _ -> + selectedEntity = parent.getItemAtPosition(position) as Entity? + } + + private var addWidgetButtonClickListener = View.OnClickListener { + try { + + val context = this@MediaPlayerControlsWidgetConfigureActivity + + // Set up a broadcast intent and pass the service call data as extras + val intent = Intent() + intent.action = MediaPlayerControlsWidget.RECEIVE_DATA + intent.component = ComponentName(context, MediaPlayerControlsWidget::class.java) + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + + intent.putExtra( + MediaPlayerControlsWidget.EXTRA_ENTITY_ID, + selectedEntity!!.entityId + ) + intent.putExtra( + MediaPlayerControlsWidget.EXTRA_LABEL, + label.text.toString() + ) + intent.putExtra( + MediaPlayerControlsWidget.EXTRA_SHOW_SKIP, + widget_show_skip_buttons_checkbox.isChecked + ) + intent.putExtra( + MediaPlayerControlsWidget.EXTRA_SHOW_SEEK, + widget_show_seek_buttons_checkbox.isChecked + ) + + context.sendBroadcast(intent) + + // Make sure we pass back the original appWidgetId + setResult( + RESULT_OK, + Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + ) + finish() + } catch (e: Exception) { + Log.e(TAG, "Issue configuring widget", e) + Toast.makeText(applicationContext, R.string.widget_creation_error, Toast.LENGTH_LONG) + .show() + } + } + + override fun onDestroy() { + mainScope.cancel() + super.onDestroy() + } +} diff --git a/app/src/main/res/drawable/ic_fastforward.xml b/app/src/main/res/drawable/ic_fastforward.xml new file mode 100644 index 000000000..c169c24e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_fastforward.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_next_track.xml b/app/src/main/res/drawable/ic_next_track.xml new file mode 100644 index 000000000..8f9fb670d --- /dev/null +++ b/app/src/main/res/drawable/ic_next_track.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_playpause.xml b/app/src/main/res/drawable/ic_playpause.xml new file mode 100644 index 000000000..ef6d23543 --- /dev/null +++ b/app/src/main/res/drawable/ic_playpause.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_prev_track.xml b/app/src/main/res/drawable/ic_prev_track.xml new file mode 100644 index 000000000..258e64c3b --- /dev/null +++ b/app/src/main/res/drawable/ic_prev_track.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_rewind.xml b/app/src/main/res/drawable/ic_rewind.xml new file mode 100644 index 000000000..fedb72db2 --- /dev/null +++ b/app/src/main/res/drawable/ic_rewind.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/widget_example_media_player_controls.png b/app/src/main/res/drawable/widget_example_media_player_controls.png new file mode 100644 index 0000000000000000000000000000000000000000..3e429298cea66377389d1eae57438a42fb20e314 GIT binary patch literal 7300 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 eVS79+1NeWz8%+RYa`mSG0000 + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_media_controls_configure.xml b/app/src/main/res/layout/widget_media_controls_configure.xml new file mode 100644 index 000000000..07bfdd208 --- /dev/null +++ b/app/src/main/res/layout/widget_media_controls_configure.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da26ad6b8..38a30551d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,6 +330,7 @@ like to connect to: Service Button A custom component is preventing service data from loading. Unable to create widget. + Unable to fetch data for configured entity. Home Assistant Widget Label No separator @@ -343,5 +344,13 @@ like to connect to: Service 30 Widget text size: - This tag does not contain Home Assistant data + Show Skip Buttons + Show Seek Buttons + Media Playing Preview Image + Previous Track + Rewind + Play/Pause + Fast Forward + Next Track + This tag does not contain Home Assistant data diff --git a/app/src/main/res/xml/media_player_control_widget_info.xml b/app/src/main/res/xml/media_player_control_widget_info.xml new file mode 100644 index 000000000..7a97a677d --- /dev/null +++ b/app/src/main/res/xml/media_player_control_widget_info.xml @@ -0,0 +1,10 @@ + + diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 380723a7c..e44c63a95 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -81,6 +81,9 @@ object Config { const val okhttp = "com.squareup.okhttp3:okhttp:$okhttpVersion" const val okhttpInterceptor = "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" const val okhttpMockServer = "com.squareup.okhttp3:mockwebserver:$okhttpVersion" + + private const val picassoVersion = "2.8" + const val picasso = "com.squareup.picasso:picasso:${picassoVersion}" } object Testing {