diff --git a/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt b/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt index e5696427..a0e5cc5a 100644 --- a/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt +++ b/mobile/src/main/java/com/thewizrd/simplewear/media/MediaControllerService.kt @@ -24,6 +24,7 @@ import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.PowerManager +import android.os.SystemClock import android.provider.MediaStore import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaMetadataCompat @@ -804,6 +805,28 @@ class MediaControllerService : Service(), MessageClient.OnMessageReceivedListene if (!isNotificationListenerEnabled(messageEvent)) return mController?.transportControls?.skipToNext() } + MediaHelper.MediaSeekForwardPath -> { + if (!isNotificationListenerEnabled(messageEvent)) return + val state = mController?.playbackState ?: return + val currentPositionMs = if (state.state == android.support.v4.media.session.PlaybackStateCompat.STATE_PLAYING) { + val elapsed = SystemClock.elapsedRealtime() - state.lastPositionUpdateTime + state.position + (elapsed * state.playbackSpeed).toLong() + } else { + state.position + } + mController?.transportControls?.seekTo((currentPositionMs + 10_000L).coerceAtLeast(0L)) + } + MediaHelper.MediaSeekBackwardPath -> { + if (!isNotificationListenerEnabled(messageEvent)) return + val state = mController?.playbackState ?: return + val currentPositionMs = if (state.state == android.support.v4.media.session.PlaybackStateCompat.STATE_PLAYING) { + val elapsed = SystemClock.elapsedRealtime() - state.lastPositionUpdateTime + state.position + (elapsed * state.playbackSpeed).toLong() + } else { + state.position + } + mController?.transportControls?.seekTo((currentPositionMs - 10_000L).coerceAtLeast(0L)) + } MediaHelper.MediaPlayFromSearchPath -> { if (!isNotificationListenerEnabled(messageEvent)) return playFromSearchController(null) diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt index 0a50af8c..f0917cae 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/helpers/MediaHelper.kt @@ -26,6 +26,9 @@ object MediaHelper { const val MediaPlayFromSearchPath = "/media/searchplay" const val MediaPlayerDisconnectPath = "/media/disconnect" + const val MediaSeekForwardPath = "/media/action/seek_forward" + const val MediaSeekBackwardPath = "/media/action/seek_backward" + const val MediaVolumeUpPath = "/media/volume/up" const val MediaVolumeDownPath = "/media/volume/down" const val MediaVolumeStatusPath = "/media/volume/status" diff --git a/shared_resources/src/main/res/drawable/ic_forward_10_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_forward_10_white_24dp.xml new file mode 100644 index 00000000..fbefb603 --- /dev/null +++ b/shared_resources/src/main/res/drawable/ic_forward_10_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/shared_resources/src/main/res/drawable/ic_replay_10_white_24dp.xml b/shared_resources/src/main/res/drawable/ic_replay_10_white_24dp.xml new file mode 100644 index 00000000..48b66699 --- /dev/null +++ b/shared_resources/src/main/res/drawable/ic_replay_10_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt b/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt index 7c520be0..affeee24 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/media/MediaPlayerViewModel.kt @@ -538,6 +538,14 @@ class MediaPlayerViewModel(app: Application) : WearableListenerViewModel(app) { requestMediaAction(MediaHelper.MediaNextPath) } + fun requestSeekForward() { + requestMediaAction(MediaHelper.MediaSeekForwardPath) + } + + fun requestSeekBackward() { + requestMediaAction(MediaHelper.MediaSeekBackwardPath) + } + fun requestVolumeUp() { requestMediaAction(MediaHelper.MediaVolumeUpPath) } diff --git a/wear/src/main/java/com/thewizrd/simplewear/media/PlayerUiController.kt b/wear/src/main/java/com/thewizrd/simplewear/media/PlayerUiController.kt index 80d6b131..08b245a2 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/media/PlayerUiController.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/media/PlayerUiController.kt @@ -5,6 +5,8 @@ interface PlayerUiController { fun pause() fun skipToPreviousMedia() fun skipToNextMedia() + fun seekForward() + fun seekBackward() } class NoopPlayerUiController : PlayerUiController { @@ -15,6 +17,10 @@ class NoopPlayerUiController : PlayerUiController { override fun skipToPreviousMedia() {} override fun skipToNextMedia() {} + + override fun seekForward() {} + + override fun seekBackward() {} } class MediaPlayerUiController(private val mediaPlayerViewModel: MediaPlayerViewModel) : @@ -26,4 +32,8 @@ class MediaPlayerUiController(private val mediaPlayerViewModel: MediaPlayerViewM override fun skipToPreviousMedia() = mediaPlayerViewModel.requestSkipToPreviousAction() override fun skipToNextMedia() = mediaPlayerViewModel.requestSkipToNextAction() + + override fun seekForward() = mediaPlayerViewModel.requestSeekForward() + + override fun seekBackward() = mediaPlayerViewModel.requestSeekBackward() } \ No newline at end of file diff --git a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt index 387ae4f5..0470de54 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/ui/simplewear/MediaPlayerUi.kt @@ -24,13 +24,16 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Forward10 import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.icons.rounded.Replay10 import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -561,27 +564,62 @@ private fun MediaPlayerControlsPage( controlButtons = { if (!isAmbient) { CompositionLocalProvider(LocalTimestampProvider provides timestampProvider) { - AnimatedMediaControlButtons( - onPlayButtonClick = { - playerUiController.play() - }, - onPauseButtonClick = { - playerUiController.pause() - }, - playPauseButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, - playing = playerState.playbackState == PlaybackState.PLAYING, - onSeekToPreviousButtonClick = { - playerUiController.skipToPreviousMedia() - }, - seekToPreviousButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, - onSeekToNextButtonClick = { - playerUiController.skipToNextMedia() - }, - seekToNextButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, - trackPositionUiModel = TrackPositionUiModelMapper.map( - playbackStateEvent + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth(0.82f), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + FilledIconButton( + onClick = { playerUiController.seekBackward() }, + modifier = Modifier.size(32.dp), + enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING + ) { + Icon( + imageVector = Icons.Rounded.Replay10, + contentDescription = "-10s", + modifier = Modifier.size(20.dp) + ) + } + FilledIconButton( + onClick = { playerUiController.seekForward() }, + modifier = Modifier.size(32.dp), + enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING + ) { + Icon( + imageVector = Icons.Rounded.Forward10, + contentDescription = "+10s", + modifier = Modifier.size(20.dp) + ) + } + } + AnimatedMediaControlButtons( + modifier = Modifier.offset(y = (-8).dp), + onPlayButtonClick = { + playerUiController.play() + }, + onPauseButtonClick = { + playerUiController.pause() + }, + playPauseButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, + playing = playerState.playbackState == PlaybackState.PLAYING, + onSeekToPreviousButtonClick = { + playerUiController.skipToPreviousMedia() + }, + seekToPreviousButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, + onSeekToNextButtonClick = { + playerUiController.skipToNextMedia() + }, + seekToNextButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, + trackPositionUiModel = TrackPositionUiModelMapper.map( + playbackStateEvent + ) ) - ) + } } } else { val leftButtonPadding = @@ -599,22 +637,54 @@ private fun MediaPlayerControlsPage( playPauseButtonEnabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, playing = playerState.playbackState == PlaybackState.PLAYING, leftButton = { - AmbientSeekToPreviousButton( - onClick = { - playerUiController.skipToPreviousMedia() - }, - buttonPadding = leftButtonPadding, - enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledIconButton( + onClick = { playerUiController.seekBackward() }, + modifier = Modifier.size(28.dp), + enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING + ) { + Icon( + imageVector = Icons.Rounded.Replay10, + contentDescription = "-10s", + modifier = Modifier.size(18.dp) + ) + } + Spacer(modifier = Modifier.height(4.dp)) + AmbientSeekToPreviousButton( + onClick = { + playerUiController.skipToPreviousMedia() + }, + buttonPadding = leftButtonPadding, + enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, + ) + } }, rightButton = { - AmbientSeekToNextButton( - onClick = { - playerUiController.skipToNextMedia() - }, - buttonPadding = rightButtonPadding, - enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + FilledIconButton( + onClick = { playerUiController.seekForward() }, + modifier = Modifier.size(28.dp), + enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING + ) { + Icon( + imageVector = Icons.Rounded.Forward10, + contentDescription = "+10s", + modifier = Modifier.size(18.dp) + ) + } + Spacer(modifier = Modifier.height(4.dp)) + AmbientSeekToNextButton( + onClick = { + playerUiController.skipToNextMedia() + }, + buttonPadding = rightButtonPadding, + enabled = !uiState.isPlaybackLoading || playerState.playbackState > PlaybackState.LOADING, + ) + } } ) } diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt index 7a5e0663..3f1b6fd4 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileMessenger.kt @@ -53,7 +53,9 @@ class MediaPlayerTileMessenger( PREVIOUS, NEXT, VOL_UP, - VOL_DOWN + VOL_DOWN, + SEEK_BACKWARD, + SEEK_FORWARD } private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -208,6 +210,14 @@ class MediaPlayerTileMessenger( PlayerAction.VOL_DOWN -> { sendMessage(mPhoneNodeWithApp!!.id, MediaHelper.MediaVolumeDownPath, null) } + + PlayerAction.SEEK_BACKWARD -> { + sendMessage(mPhoneNodeWithApp!!.id, MediaHelper.MediaSeekBackwardPath, null) + } + + PlayerAction.SEEK_FORWARD -> { + sendMessage(mPhoneNodeWithApp!!.id, MediaHelper.MediaSeekForwardPath, null) + } } } } diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt index f5ecd7ce..d0874379 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/MediaPlayerTileRenderer.kt @@ -43,6 +43,8 @@ class MediaPlayerTileRenderer(context: Context, debugResourceMode: Boolean = fal internal const val ID_VOL_UP = "vol_up" internal const val ID_VOL_DOWN = "vol_down" internal const val ID_APPICON = "app_icon" + internal const val ID_SEEK_BACKWARD = "seek_backward" + internal const val ID_SEEK_FORWARD = "seek_forward" fun getTapAction(context: Context): ActionBuilders.Action { return ActionBuilders.launchAction( @@ -94,7 +96,9 @@ class MediaPlayerTileRenderer(context: Context, debugResourceMode: Boolean = fal ID_SKIP to R.drawable.ic_baseline_skip_next_24, ID_VOL_UP to R.drawable.ic_volume_up_white_24dp, - ID_VOL_DOWN to R.drawable.ic_baseline_volume_down_24 + ID_VOL_DOWN to R.drawable.ic_baseline_volume_down_24, + ID_SEEK_BACKWARD to R.drawable.ic_replay_10_white_24dp, + ID_SEEK_FORWARD to R.drawable.ic_forward_10_white_24dp ) (resourceIds.takeIf { it.isNotEmpty() } ?: resources.keys).forEach { key -> diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt index 527eb16a..d1a2b818 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/layouts/MediaPlayerTileLayout.kt @@ -85,6 +85,8 @@ import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion. import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.ID_SKIP import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.ID_VOL_DOWN import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.ID_VOL_UP +import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.ID_SEEK_BACKWARD +import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileRenderer.Companion.ID_SEEK_FORWARD import com.thewizrd.simplewear.wearable.tiles.MediaPlayerTileState import kotlinx.coroutines.runBlocking import java.time.Instant @@ -345,15 +347,9 @@ internal fun MediaPlayerTileLayout( Box.Builder() .setWidth(expand()) .setHeight( - WrappedDimensionProp.Builder() - .apply { - if (deviceParameters.isLargeHeight()) { - setMinimumSize(dp(80f)) - } else { - setMinimumSize(dp(64f)) - } - } - .build() + dp( + if (deviceParameters.isLargeHeight()) 80f else 64f + ) ) .addContent( buttonGroup( @@ -361,6 +357,10 @@ internal fun MediaPlayerTileLayout( width = expand(), spacing = 0f ) { + buttonGroupItem { + SeekButton(PlayerAction.SEEK_BACKWARD) + } + buttonGroupItem { PlayerButton(action = PlayerAction.PREVIOUS) } @@ -372,6 +372,10 @@ internal fun MediaPlayerTileLayout( buttonGroupItem { PlayerButton(action = PlayerAction.NEXT) } + + buttonGroupItem { + SeekButton(PlayerAction.SEEK_FORWARD) + } } ) .build() @@ -549,6 +553,24 @@ private fun DeviceParameters.getScreenWidthInDpFromPercentage( return ceil(screenWidthDp * percent / 100f) } +private fun MaterialScope.SeekButton( + action: PlayerAction +): LayoutElement { + val size = if (deviceConfiguration.isLargeHeight()) 28f else 24f + return iconButton( + onClick = clickable(id = action.name), + width = dp(size), + height = dp(size), + iconContent = { + icon( + protoLayoutResourceId = getResourceIdForPlayerAction(action), + width = dp(size - 8f), + height = dp(size - 8f) + ) + } + ) +} + private fun MaterialScope.PlayerButton( action: PlayerAction ): LayoutElement { @@ -792,6 +814,8 @@ private fun getResourceIdForPlayerAction(action: PlayerAction): String { PlayerAction.NEXT -> ID_SKIP PlayerAction.VOL_UP -> ID_VOL_UP PlayerAction.VOL_DOWN -> ID_VOL_DOWN + PlayerAction.SEEK_BACKWARD -> ID_SEEK_BACKWARD + PlayerAction.SEEK_FORWARD -> ID_SEEK_FORWARD } } diff --git a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/unofficial/MediaPlayerTileProviderService.kt b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/unofficial/MediaPlayerTileProviderService.kt index dd417571..0e13fef7 100644 --- a/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/unofficial/MediaPlayerTileProviderService.kt +++ b/wear/src/main/java/com/thewizrd/simplewear/wearable/tiles/unofficial/MediaPlayerTileProviderService.kt @@ -249,6 +249,15 @@ class MediaPlayerTileProviderService : TileProviderService() { getActionClickIntent(this, MediaHelper.MediaNextPath) ) + views.setOnClickPendingIntent( + R.id.seek_backward_button, + getActionClickIntent(this, MediaHelper.MediaSeekBackwardPath) + ) + views.setOnClickPendingIntent( + R.id.seek_forward_button, + getActionClickIntent(this, MediaHelper.MediaSeekForwardPath) + ) + views.setOnClickPendingIntent( R.id.vol_down_button, getActionClickIntent(this, MediaHelper.MediaVolumeDownPath) @@ -287,6 +296,8 @@ class MediaPlayerTileProviderService : TileProviderService() { MediaHelper.MediaNextPath -> requestPlayerAction(PlayerAction.NEXT) MediaHelper.MediaVolumeUpPath -> requestPlayerAction(PlayerAction.VOL_UP) MediaHelper.MediaVolumeDownPath -> requestPlayerAction(PlayerAction.VOL_DOWN) + MediaHelper.MediaSeekBackwardPath -> requestPlayerAction(PlayerAction.SEEK_BACKWARD) + MediaHelper.MediaSeekForwardPath -> requestPlayerAction(PlayerAction.SEEK_FORWARD) } return super.onStartCommand(intent, flags, startId) diff --git a/wear/src/main/res/layout/tile_mediaplayer.xml b/wear/src/main/res/layout/tile_mediaplayer.xml index 1833ea95..a8d6b04d 100644 --- a/wear/src/main/res/layout/tile_mediaplayer.xml +++ b/wear/src/main/res/layout/tile_mediaplayer.xml @@ -146,10 +146,46 @@ + + + + + + + +