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.Square.okhttp)
|
||||
implementation(Config.Dependency.Square.picasso)
|
||||
|
||||
"fullImplementation"(Config.Dependency.Play.location)
|
||||
"fullImplementation"(Config.Dependency.Firebase.core)
|
||||
|
|
|
@ -78,6 +78,23 @@
|
|||
android:resource="@xml/entity_widget_info" />
|
||||
</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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
@ -107,7 +124,11 @@
|
|||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</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">
|
||||
<intent-filter>
|
||||
<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.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
|
||||
|
|
|
@ -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.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)
|
||||
}
|
||||
|
|
|
@ -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_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_entity_fetch_error">Unable to fetch data for configured entity.</string>
|
||||
<string name="widget_image_description">Home Assistant Widget</string>
|
||||
<string name="widget_label_placeholder_text">Label</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_size_default">30</string>
|
||||
<string name="widget_text_size_label">Widget text size:</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>
|
||||
|
|
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 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 {
|
||||
|
|
Loading…
Reference in a new issue