mirror of
https://github.com/home-assistant/android
synced 2024-09-18 23:52:51 +00:00
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 <Bassett.JustinT@gmail.com>
This commit is contained in:
parent
cce1608bf0
commit
51072b9e0d
|
@ -133,6 +133,7 @@ dependencies {
|
||||||
|
|
||||||
implementation(Config.Dependency.Misc.jackson)
|
implementation(Config.Dependency.Misc.jackson)
|
||||||
implementation(Config.Dependency.Square.okhttp)
|
implementation(Config.Dependency.Square.okhttp)
|
||||||
|
implementation(Config.Dependency.Square.picasso)
|
||||||
|
|
||||||
"fullImplementation"(Config.Dependency.Play.location)
|
"fullImplementation"(Config.Dependency.Play.location)
|
||||||
"fullImplementation"(Config.Dependency.Firebase.core)
|
"fullImplementation"(Config.Dependency.Firebase.core)
|
||||||
|
|
|
@ -78,6 +78,23 @@
|
||||||
android:resource="@xml/entity_widget_info" />
|
android:resource="@xml/entity_widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".widgets.media_player_controls.MediaPlayerControlsWidget">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.RECEIVE_DATA" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.UPDATE_MEDIA_IMAGE" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_PREV_TRACK" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_REWIND" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_PLAYPAUSE" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_FASTFORWARD" />
|
||||||
|
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_NEXT_TRACK" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/media_player_control_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<receiver android:name=".widgets.template.TemplateWidget" android:label="Template Widget">
|
<receiver android:name=".widgets.template.TemplateWidget" android:label="Template Widget">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
@ -107,7 +124,11 @@
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity android:name=".widgets.media_player_controls.MediaPlayerControlsWidgetConfigureActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity android:name=".widgets.template.TemplateWidgetConfigureActivity">
|
<activity android:name=".widgets.template.TemplateWidgetConfigureActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||||
|
|
|
@ -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.sensor.Setting
|
||||||
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
|
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
|
||||||
import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity
|
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.StaticWidgetDao
|
||||||
import io.homeassistant.companion.android.database.widget.StaticWidgetEntity
|
import io.homeassistant.companion.android.database.widget.StaticWidgetEntity
|
||||||
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
|
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
|
||||||
|
@ -38,15 +40,17 @@ import kotlinx.coroutines.runBlocking
|
||||||
Sensor::class,
|
Sensor::class,
|
||||||
Setting::class,
|
Setting::class,
|
||||||
ButtonWidgetEntity::class,
|
ButtonWidgetEntity::class,
|
||||||
|
MediaPlayerControlsWidgetEntity::class,
|
||||||
StaticWidgetEntity::class,
|
StaticWidgetEntity::class,
|
||||||
TemplateWidgetEntity::class
|
TemplateWidgetEntity::class
|
||||||
],
|
],
|
||||||
version = 11
|
version = 12
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun authenticationDao(): AuthenticationDao
|
abstract fun authenticationDao(): AuthenticationDao
|
||||||
abstract fun sensorDao(): SensorDao
|
abstract fun sensorDao(): SensorDao
|
||||||
abstract fun buttonWidgetDao(): ButtonWidgetDao
|
abstract fun buttonWidgetDao(): ButtonWidgetDao
|
||||||
|
abstract fun mediaPlayCtrlWidgetDao(): MediaPlayerControlsWidgetDao
|
||||||
abstract fun staticWidgetDao(): StaticWidgetDao
|
abstract fun staticWidgetDao(): StaticWidgetDao
|
||||||
abstract fun templateWidgetDao(): TemplateWidgetDao
|
abstract fun templateWidgetDao(): TemplateWidgetDao
|
||||||
|
|
||||||
|
@ -83,7 +87,8 @@ abstract class AppDatabase : RoomDatabase() {
|
||||||
MIGRATION_7_8,
|
MIGRATION_7_8,
|
||||||
MIGRATION_8_9,
|
MIGRATION_8_9,
|
||||||
MIGRATION_9_10,
|
MIGRATION_9_10,
|
||||||
MIGRATION_10_11
|
MIGRATION_10_11,
|
||||||
|
MIGRATION_11_12
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.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() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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.button.ButtonWidgetConfigureActivity
|
||||||
import io.homeassistant.companion.android.widgets.entity.EntityWidget
|
import io.homeassistant.companion.android.widgets.entity.EntityWidget
|
||||||
import io.homeassistant.companion.android.widgets.entity.EntityWidgetConfigureActivity
|
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.TemplateWidget
|
||||||
import io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigureActivity
|
import io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigureActivity
|
||||||
|
|
||||||
|
@ -13,14 +15,14 @@ import io.homeassistant.companion.android.widgets.template.TemplateWidgetConfigu
|
||||||
interface ProviderComponent {
|
interface ProviderComponent {
|
||||||
|
|
||||||
fun inject(receiver: ButtonWidget)
|
fun inject(receiver: ButtonWidget)
|
||||||
|
|
||||||
fun inject(activity: ButtonWidgetConfigureActivity)
|
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(activity: EntityWidgetConfigureActivity)
|
||||||
|
|
||||||
fun inject(receiver: TemplateWidget)
|
fun inject(receiver: TemplateWidget)
|
||||||
|
|
||||||
fun inject(activity: TemplateWidgetConfigureActivity)
|
fun inject(activity: TemplateWidgetConfigureActivity)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Map<String, Any>>
|
||||||
|
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<String, Any> = 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<Map<String, Any>>
|
||||||
|
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<String, Any> = 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<String, Any> = 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<Map<String, Any>>
|
||||||
|
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<String, Any> = 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<String, Any> = 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!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Entity<Any>>()
|
||||||
|
private var selectedEntity: Entity<Any>? = 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<Entity<Any>>(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<Any>?
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/res/drawable/ic_fastforward.xml
Normal file
8
app/src/main/res/drawable/ic_fastforward.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M13,6V18L21.5,12M4,18L12.5,12L4,6V18Z" />
|
||||||
|
</vector>
|
8
app/src/main/res/drawable/ic_next_track.xml
Normal file
8
app/src/main/res/drawable/ic_next_track.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M16,18H18V6H16M6,18L14.5,12L6,6V18Z" />
|
||||||
|
</vector>
|
8
app/src/main/res/drawable/ic_playpause.xml
Normal file
8
app/src/main/res/drawable/ic_playpause.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M3,5V19L11,12M13,19H16V5H13M18,5V19H21V5" />
|
||||||
|
</vector>
|
8
app/src/main/res/drawable/ic_prev_track.xml
Normal file
8
app/src/main/res/drawable/ic_prev_track.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M6,18V6H8V18H6M9.5,12L18,6V18L9.5,12Z" />
|
||||||
|
</vector>
|
8
app/src/main/res/drawable/ic_rewind.xml
Normal file
8
app/src/main/res/drawable/ic_rewind.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M11.5,12L20,18V6M11,18V6L2.5,12L11,18Z" />
|
||||||
|
</vector>
|
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
130
app/src/main/res/layout/widget_media_controls.xml
Normal file
130
app/src/main/res/layout/widget_media_controls.xml
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/widgetLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:minWidth="40dp"
|
||||||
|
android:background="@drawable/widget_button_background">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/widgetImageButtonLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetMediaImage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="invisible"
|
||||||
|
android:contentDescription="@string/widget_media_image_description"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetMediaPlaceholder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:src="@drawable/app_icon"
|
||||||
|
android:contentDescription="@string/widget_media_image_description"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/widgetLabelLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:minHeight="32sp"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widgetLabel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/widget_label_placeholder_text"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/colorWidgetButtonLabel"
|
||||||
|
android:minLines="1"
|
||||||
|
android:maxLines="2"
|
||||||
|
/>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/widgetMediaButtonlayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_below="@id/widgetLabel"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/widgetPrevTrackButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:contentDescription="@string/widget_media_prev_track_button"
|
||||||
|
android:src="@drawable/ic_prev_track"
|
||||||
|
android:tint="@color/colorIcon"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:selectableItemBackground"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/widgetRewindButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:contentDescription="@string/widget_media_rewind_button"
|
||||||
|
android:src="@drawable/ic_rewind"
|
||||||
|
android:tint="@color/colorIcon"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:selectableItemBackground"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/widgetPlayPauseButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1.5"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:contentDescription="@string/widget_media_playpause_button"
|
||||||
|
android:src="@drawable/ic_playpause"
|
||||||
|
android:tint="@color/colorIcon"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:selectableItemBackground"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/widgetFastForwardButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:contentDescription="@string/widget_media_fastforward_button"
|
||||||
|
android:src="@drawable/ic_fastforward"
|
||||||
|
android:tint="@color/colorIcon"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:selectableItemBackground"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/widgetNextTrackButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:contentDescription="@string/widget_media_next_track_button"
|
||||||
|
android:src="@drawable/ic_next_track"
|
||||||
|
android:tint="@color/colorIcon"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?android:selectableItemBackground"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
91
app/src/main/res/layout/widget_media_controls_configure.xml
Normal file
91
app/src/main/res/layout/widget_media_controls_configure.xml
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/TextAppearance.HomeAssistant.Headline"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/select_entity_to_display" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:labelFor="@id/widget_text_config_entity_id"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:text="@string/label_entity_id" />
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/widget_text_config_entity_id"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:completionThreshold="0"
|
||||||
|
android:imeOptions="actionNext"
|
||||||
|
android:inputType="text" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/widget_show_skip_buttons_checkbox"
|
||||||
|
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_skip" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/widget_show_seek_buttons_checkbox"
|
||||||
|
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_seek" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:text="@string/label_label" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
|
android:id="@+id/label"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/widget_text_hint_label"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="text" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/add_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/add_widget" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
|
@ -330,6 +330,7 @@ like to connect to:</string>
|
||||||
<string name="widget_button_image_description">Service Button</string>
|
<string name="widget_button_image_description">Service Button</string>
|
||||||
<string name="widget_config_service_error">A custom component is preventing service data from loading.</string>
|
<string name="widget_config_service_error">A custom component is preventing service data from loading.</string>
|
||||||
<string name="widget_creation_error">Unable to create widget.</string>
|
<string name="widget_creation_error">Unable to create widget.</string>
|
||||||
|
<string name="widget_entity_fetch_error">Unable to fetch data for configured entity.</string>
|
||||||
<string name="widget_image_description">Home Assistant Widget</string>
|
<string name="widget_image_description">Home Assistant Widget</string>
|
||||||
<string name="widget_label_placeholder_text">Label</string>
|
<string name="widget_label_placeholder_text">Label</string>
|
||||||
<string name="widget_separator_input_hint">No separator</string>
|
<string name="widget_separator_input_hint">No separator</string>
|
||||||
|
@ -343,5 +344,13 @@ like to connect to:</string>
|
||||||
<string name="widget_text_hint_service_service">Service</string>
|
<string name="widget_text_hint_service_service">Service</string>
|
||||||
<string name="widget_text_size_default">30</string>
|
<string name="widget_text_size_default">30</string>
|
||||||
<string name="widget_text_size_label">Widget text size:</string>
|
<string name="widget_text_size_label">Widget text size:</string>
|
||||||
<string name="nfc_invalid_tag">This tag does not contain Home Assistant data</string>
|
<string name="widget_media_show_skip">Show Skip Buttons</string>
|
||||||
|
<string name="widget_media_show_seek">Show Seek Buttons</string>
|
||||||
|
<string name="widget_media_image_description">Media Playing Preview Image</string>
|
||||||
|
<string name="widget_media_prev_track_button">Previous Track</string>
|
||||||
|
<string name="widget_media_rewind_button">Rewind</string>
|
||||||
|
<string name="widget_media_playpause_button">Play/Pause</string>
|
||||||
|
<string name="widget_media_fastforward_button">Fast Forward</string>
|
||||||
|
<string name="widget_media_next_track_button">Next Track</string>
|
||||||
|
<string name="nfc_invalid_tag">This tag does not contain Home Assistant data</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
10
app/src/main/res/xml/media_player_control_widget_info.xml
Normal file
10
app/src/main/res/xml/media_player_control_widget_info.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:configure="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidgetConfigureActivity"
|
||||||
|
android:initialKeyguardLayout="@layout/widget_media_controls"
|
||||||
|
android:initialLayout="@layout/widget_media_controls"
|
||||||
|
android:minWidth="250dp"
|
||||||
|
android:minHeight="40dp"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:widgetCategory="home_screen"
|
||||||
|
android:previewImage="@drawable/widget_example_media_player_controls" />
|
|
@ -81,6 +81,9 @@ object Config {
|
||||||
const val okhttp = "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
const val okhttp = "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
const val okhttpInterceptor = "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
const val okhttpInterceptor = "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
||||||
const val okhttpMockServer = "com.squareup.okhttp3:mockwebserver:$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 {
|
object Testing {
|
||||||
|
|
Loading…
Reference in a new issue