Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission
android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<!-- MANAGE_EXTERNAL_STORAGE removed for Google Play compliance.
Write access to individual files is obtained via MediaStore.createWriteRequest(). -->
<!-- Required only on Android 11 and below for legacy Bluetooth discovery and Wi-Fi SSID access. -->
Expand Down
134 changes: 76 additions & 58 deletions app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectTapGestures
import com.theveloper.pixelplay.presentation.viewmodel.PlayerSheetState

import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand Down Expand Up @@ -779,7 +782,6 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
bottomBar = {
if (shouldRenderNavigationBar) {
val playerContentExpansionFraction = playerViewModel.playerContentExpansionFraction.value
val currentSongId by remember {
playerViewModel.stablePlayerState
.map { it.currentSong?.id }
Expand All @@ -788,58 +790,31 @@ class MainActivity : ComponentActivity() {
val showPlayerContentArea = currentSongId != null
val navBarElevation = 3.dp

val playerContentActualBottomRadiusTargetValue by remember(
navBarStyle,
showPlayerContentArea,
playerContentExpansionFraction,
navBarCornerRadius
) {
derivedStateOf {
if (navBarStyle == NavBarStyle.FULL_WIDTH) {
return@derivedStateOf lerp(navBarCornerRadius.dp, 26.dp, playerContentExpansionFraction)
}

if (showPlayerContentArea) {
if (playerContentExpansionFraction < 0.2f) {
lerp(navBarCornerRadius.dp, 26.dp, (playerContentExpansionFraction / 0.2f).coerceIn(0f, 1f))
} else {
26.dp
}
} else {
navBarCornerRadius.dp
}
}
}

val playerContentActualBottomRadius by animateDpAsState(
targetValue = playerContentActualBottomRadiusTargetValue,
animationSpec = androidx.compose.animation.core.spring(
dampingRatio = androidx.compose.animation.core.Spring.DampingRatioNoBouncy,
stiffness = androidx.compose.animation.core.Spring.StiffnessMedium
),
label = "PlayerContentBottomRadius"
)

val navBarHideFraction = if (showPlayerContentArea) playerContentExpansionFraction else 0f
val navBarHideFractionClamped = navBarHideFraction.coerceIn(0f, 1f)

val animatedNavBarCornerRadius by animateDpAsState(
targetValue = navBarCornerRadius.dp,
animationSpec = tween(400),
label = "NavBarCornerRadius"
)

val actualShape = remember(playerContentActualBottomRadius, showPlayerContentArea, navBarStyle, animatedNavBarCornerRadius) {
val bottomRadius = if (navBarStyle == NavBarStyle.FULL_WIDTH) 0.dp else animatedNavBarCornerRadius
AbsoluteSmoothCornerShape(
cornerRadiusTL = playerContentActualBottomRadius,
smoothnessAsPercentBR = 60,
cornerRadiusTR = playerContentActualBottomRadius,
smoothnessAsPercentTL = 60,
cornerRadiusBL = bottomRadius,
smoothnessAsPercentTR = 60,
cornerRadiusBR = bottomRadius,
smoothnessAsPercentBL = 60
val actualShape = remember(navBarStyle, showPlayerContentArea, navBarCornerRadius, animatedNavBarCornerRadius) {
DynamicSmoothCornerShape(
topRadiusProvider = {
val fraction = playerViewModel.playerContentExpansionFraction.value
if (navBarStyle == NavBarStyle.FULL_WIDTH) {
lerp(navBarCornerRadius.dp, 26.dp, fraction)
} else if (showPlayerContentArea) {
if (fraction < 0.2f) {
lerp(navBarCornerRadius.dp, 26.dp, (fraction / 0.2f).coerceIn(0f, 1f))
} else {
26.dp
}
} else {
navBarCornerRadius.dp
}
},
bottomRadiusProvider = {
if (navBarStyle == NavBarStyle.FULL_WIDTH) 0.dp else animatedNavBarCornerRadius
}
)
}

Expand All @@ -851,16 +826,6 @@ class MainActivity : ComponentActivity() {
val bottomBarPaddingPx = remember(bottomBarPadding, density) {
with(density) { bottomBarPadding.toPx() }
}
val animatedTranslationY by remember(
navBarHideFractionClamped,
componentHeightPx,
shadowOverflowPx,
bottomBarPaddingPx,
) {
derivedStateOf {
(componentHeightPx + shadowOverflowPx + bottomBarPaddingPx) * navBarHideFractionClamped
}
}

Box(
modifier = Modifier
Expand All @@ -879,7 +844,12 @@ class MainActivity : ComponentActivity() {
.padding(bottom = bottomBarPadding)
.onSizeChanged { componentHeightPx = it.height }
.graphicsLayer {
translationY = animatedTranslationY
val hideFraction = if (showPlayerContentArea) {
playerViewModel.playerContentExpansionFraction.value.coerceIn(0f, 1f)
} else {
0f
}
translationY = (componentHeightPx + shadowOverflowPx + bottomBarPaddingPx) * hideFraction
alpha = 1f
}
.height(navBarHeight)
Expand Down Expand Up @@ -943,6 +913,29 @@ class MainActivity : ComponentActivity() {
onOpenSidebar = { scope.launch { drawerState.open() } }
)

val isExpandedOrExpanding by remember {
derivedStateOf {
playerViewModel.playerContentExpansionFraction.value > 0.01f
}
}
AnimatedVisibility(
visible = isExpandedOrExpanding,
enter = fadeIn(animationSpec = tween(durationMillis = 350)),
exit = fadeOut(animationSpec = tween(durationMillis = 350)),
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainerLowest.copy(alpha = 0.6f))
.pointerInput(Unit) {
detectTapGestures {
playerViewModel.collapsePlayerSheet()
}
}
)
}

UnifiedPlayerSheetV2(
playerViewModel = playerViewModel,
sheetCollapsedTargetY = sheetCollapsedTargetY,
Expand Down Expand Up @@ -1088,3 +1081,28 @@ class MainActivity : ComponentActivity() {


}

private class DynamicSmoothCornerShape(
private val topRadiusProvider: () -> androidx.compose.ui.unit.Dp,
private val bottomRadiusProvider: () -> androidx.compose.ui.unit.Dp
) : androidx.compose.ui.graphics.Shape {
override fun createOutline(
size: androidx.compose.ui.geometry.Size,
layoutDirection: androidx.compose.ui.unit.LayoutDirection,
density: androidx.compose.ui.unit.Density
): androidx.compose.ui.graphics.Outline {
val topRadius = topRadiusProvider()
val bottomRadius = bottomRadiusProvider()
val delegate = AbsoluteSmoothCornerShape(
cornerRadiusTL = topRadius,
smoothnessAsPercentTL = 60,
cornerRadiusTR = topRadius,
smoothnessAsPercentTR = 60,
cornerRadiusBL = bottomRadius,
smoothnessAsPercentBL = 60,
cornerRadiusBR = bottomRadius,
smoothnessAsPercentBR = 60
)
return delegate.createOutline(size, layoutDirection, density)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import com.theveloper.pixelplay.data.preferences.EqualizerPreferencesRepository
import com.theveloper.pixelplay.data.preferences.ThemePreferencesRepository
import com.theveloper.pixelplay.data.preferences.UserPreferencesRepository
import com.theveloper.pixelplay.data.repository.MusicRepository
import com.theveloper.pixelplay.data.service.cast.CastRemotePlaybackState
import com.theveloper.pixelplay.data.service.player.DualPlayerEngine
import com.theveloper.pixelplay.data.service.player.TransitionController
import com.theveloper.pixelplay.ui.glancewidget.ControlWidget4x2
Expand Down Expand Up @@ -182,6 +183,7 @@ class MusicService : MediaLibraryService() {
private var castRemoteClientCallback: RemoteMediaClient.Callback? = null
private var observedCastSession: CastSession? = null
private var activeCastStatsOccurrenceId: String? = null
private var activeCastPlaybackIntent: Boolean = false
private var playbackSnapshotPersistJob: Job? = null
private var playbackSnapshotUnloadWriteJob: Job? = null
private var isRestoringPlaybackSnapshot = false
Expand All @@ -207,6 +209,8 @@ class MusicService : MediaLibraryService() {
private const val PLAYBACK_SNAPSHOT_DEBOUNCE_MS = 1500L
private const val FORCED_WIDGET_STATE_DEBOUNCE_MS = 250L
private const val MEDIA_SESSION_BUTTON_DEBOUNCE_MS = 250L
private const val DEFERRED_SERVICE_STARTUP_WORK_DELAY_MS = 1_000L
private const val PAUSED_RESTORE_PREPARE_QUEUE_LIMIT = 50
private val pendingMediaButtonForegroundStarts = AtomicInteger(0)

private const val APP_PACKAGE_PREFIX = "com.theveloper.pixelplay"
Expand Down Expand Up @@ -404,7 +408,12 @@ class MusicService : MediaLibraryService() {
engine.addTransitionFinishedListener(transitionFinishedListener)

controller.initialize()
initializeCastWearSync()
serviceScope.launch {
delay(DEFERRED_SERVICE_STARTUP_WORK_DELAY_MS)
if (!isPlaybackUnloadInProgress && mediaSession != null) {
initializeCastWearSync()
}
}
registerHeadsetReconnectMonitor()

serviceScope.launch {
Expand Down Expand Up @@ -1612,6 +1621,7 @@ class MusicService : MediaLibraryService() {
syncCastListeningStatsFromRemote()
} ?: run {
activeCastStatsOccurrenceId = null
activeCastPlaybackIntent = false
listeningStatsTracker.onPlaybackStopped()
}
requestWidgetFullUpdate(force = true)
Expand Down Expand Up @@ -1944,9 +1954,9 @@ class MusicService : MediaLibraryService() {
resolvedIndex,
snapshot.currentPositionMs.coerceAtLeast(0L)
)
// Even paused restores must prepare the timeline so duration/seek state is
// available immediately when the UI opens after a cold start.
player.prepare()
if (shouldRestorePlaying || preparedItems.size <= PAUSED_RESTORE_PREPARE_QUEUE_LIMIT) {
player.prepare()
}
player.repeatMode = safeRepeatMode
player.shuffleModeEnabled = false
isManualShuffleEnabled = snapshot.shuffleEnabled
Expand Down Expand Up @@ -2061,6 +2071,7 @@ class MusicService : MediaLibraryService() {
val artist: String,
val artworkUri: Uri?,
val isPlaying: Boolean,
val isActuallyPlaying: Boolean,
val currentPositionMs: Long,
val totalDurationMs: Long,
val repeatMode: Int,
Expand All @@ -2082,7 +2093,7 @@ class MusicService : MediaLibraryService() {
songId = songId,
positionMs = snapshot.currentPositionMs,
durationMs = snapshot.totalDurationMs,
isPlaying = snapshot.isPlaying
isPlaying = snapshot.isActuallyPlaying
)
return
}
Expand All @@ -2091,7 +2102,7 @@ class MusicService : MediaLibraryService() {
songId = songId,
positionMs = snapshot.currentPositionMs,
durationMs = snapshot.totalDurationMs,
isPlaying = snapshot.isPlaying
isPlaying = snapshot.isActuallyPlaying
)
}

Expand Down Expand Up @@ -2144,14 +2155,20 @@ class MusicService : MediaLibraryService() {
MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE -> Player.REPEAT_MODE_ALL
else -> Player.REPEAT_MODE_OFF
}
val remotePlayback = CastRemotePlaybackState.project(
mediaStatus = mediaStatus,
previousPlayIntent = activeCastPlaybackIntent
)
activeCastPlaybackIntent = remotePlayback.playWhenReady

return RemotePlaybackSnapshot(
occurrenceId = occurrenceId,
songId = songId,
title = metadata?.getString(CastMediaMetadata.KEY_TITLE).orEmpty(),
artist = metadata?.getString(CastMediaMetadata.KEY_ARTIST).orEmpty(),
artworkUri = imageUri,
isPlaying = mediaStatus.playerState == MediaStatus.PLAYER_STATE_PLAYING,
isPlaying = remotePlayback.isPlaying,
isActuallyPlaying = mediaStatus.playerState == MediaStatus.PLAYER_STATE_PLAYING,
currentPositionMs = remoteClient.approximateStreamPosition.coerceAtLeast(0L),
totalDurationMs = effectiveDurationMs,
repeatMode = mappedRepeatMode,
Expand Down
Loading
Loading