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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ data class AudioInfo(
val isMuted: Boolean,
val albumArt: String? = null,
val albumArtLite: String? = null,
val durationMs: Long = 0L,
val positionMs: Long = 0L,
val positionTimestampMs: Long = 0L,
val isBuffering: Boolean = false,
// New: like status for current media ("liked", "not_liked", or "none")
val likeStatus: String = "none"
)
Expand All @@ -23,6 +27,10 @@ data class MediaInfo(
val artist: String,
val albumArt: String? = null,
val albumArtLite: String? = null,
val durationMs: Long = 0L,
val positionMs: Long = 0L,
val positionTimestampMs: Long = 0L,
val isBuffering: Boolean = false,
// New: like status for current media ("liked", "not_liked", or "none")
val likeStatus: String = "none"
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class MediaNotificationListener : NotificationListenerService() {
fun getMediaInfo(context: Context): MediaInfo {
// Respect global toggle; if disabled, return empty media
if (!isNowPlayingEnabled) {
return MediaInfo(false, "", "", null, "none")
return MediaInfo(false, "", "", null, null, 0L, 0L, 0L, false, "none")
}
return try {
val mediaSessionManager =
Expand Down Expand Up @@ -119,6 +119,14 @@ class MediaNotificationListener : NotificationListenerService() {
val title = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: ""
val artist = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: ""
val isPlaying = playbackState?.state == PlaybackState.STATE_PLAYING
val durationMs = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L
val positionMs = playbackState?.position ?: 0L
val positionTimestampMs = System.currentTimeMillis()
val isBuffering = when (playbackState?.state) {
PlaybackState.STATE_BUFFERING,
PlaybackState.STATE_CONNECTING -> true
else -> false
}

val albumArtBitmap =
metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
Expand Down Expand Up @@ -166,6 +174,10 @@ class MediaNotificationListener : NotificationListenerService() {
artist = artist,
albumArt = albumArtBase64,
albumArtLite = albumArtLiteBase64,
durationMs = durationMs,
positionMs = positionMs,
positionTimestampMs = positionTimestampMs,
isBuffering = isBuffering,
likeStatus = likeStatus
)
}
Expand All @@ -181,10 +193,10 @@ class MediaNotificationListener : NotificationListenerService() {
}

// Log.d(TAG, "No media info found")
MediaInfo(false, "", "", null, "none")
MediaInfo(false, "", "", null, null, 0L, 0L, 0L, false, "none")
} catch (e: Exception) {
Log.e(TAG, "Error getting media info: ${e.message}")
MediaInfo(false, "", "", null, "none")
MediaInfo(false, "", "", null, null, 0L, 0L, 0L, false, "none")
}
}

Expand Down
16 changes: 14 additions & 2 deletions app/src/main/java/com/sameerasw/airsync/utils/DeviceInfoUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ object DeviceInfoUtil {
isMuted = isMuted,
albumArt = null,
albumArtLite = null,
durationMs = 0L,
positionMs = 0L,
positionTimestampMs = 0L,
isBuffering = false,
likeStatus = "none"
)
}
Expand All @@ -168,11 +172,15 @@ object DeviceInfoUtil {
isMuted = isMuted,
albumArt = mediaInfo.albumArt,
albumArtLite = mediaInfo.albumArtLite,
durationMs = mediaInfo.durationMs,
positionMs = mediaInfo.positionMs,
positionTimestampMs = mediaInfo.positionTimestampMs,
isBuffering = mediaInfo.isBuffering,
likeStatus = mediaInfo.likeStatus
)
} catch (e: Exception) {
Log.e("DeviceInfoUtil", "Error getting audio info: ${e.message}")
AudioInfo(false, "", "", 0, true, null, "none")
AudioInfo(false, "", "", 0, true, null, null, 0L, 0L, 0L, false, "none")
}
}

Expand All @@ -191,6 +199,10 @@ object DeviceInfoUtil {
isMuted = audioInfo.isMuted,
albumArt = audioInfo.albumArt,
albumArtLite = audioInfo.albumArtLite,
duration = audioInfo.durationMs,
position = audioInfo.positionMs,
positionTimestamp = audioInfo.positionTimestampMs,
isBuffering = audioInfo.isBuffering,
likeStatus = audioInfo.likeStatus
)
}
Expand All @@ -215,4 +227,4 @@ object DeviceInfoUtil {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as? android.os.PowerManager
return powerManager?.isPowerSaveMode == true
}
}
}
8 changes: 6 additions & 2 deletions app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,15 @@ object JsonUtil {
isMuted: Boolean,
albumArt: String?,
albumArtLite: String? = null,
duration: Long = 0L,
position: Long = 0L,
positionTimestamp: Long = 0L,
isBuffering: Boolean = false,
likeStatus: String
): String {
val albumArtJson = if (albumArt != null) ",\"albumArt\":\"$albumArt\"" else ""
val albumArtLiteJson = if (albumArtLite != null) ",\"albumArtLite\":\"$albumArtLite\"" else ""
return """{"type":"status","data":{"battery":{"level":$batteryLevel,"isCharging":$isCharging},"isPaired":$isPaired,"music":{"isPlaying":$isPlaying,"title":"$title","artist":"$artist","volume":$volume,"isMuted":$isMuted$albumArtJson$albumArtLiteJson,"likeStatus":"$likeStatus"}}}"""
return """{"type":"status","data":{"battery":{"level":$batteryLevel,"isCharging":$isCharging},"isPaired":$isPaired,"music":{"isPlaying":$isPlaying,"title":"$title","artist":"$artist","volume":$volume,"isMuted":$isMuted$albumArtJson$albumArtLiteJson,"duration":$duration,"position":$position,"positionTimestamp":$positionTimestamp,"isBuffering":$isBuffering,"likeStatus":"$likeStatus"}}}"""
}

/**
Expand Down Expand Up @@ -266,4 +270,4 @@ object JsonUtil {
)
}"}}"""
}
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/sameerasw/airsync/utils/MediaControlUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ object MediaControlUtil {
}
}

/**
* Seek active media playback to an absolute position in milliseconds.
*/
fun seekTo(context: Context, positionMs: Long): Boolean {
return try {
val controller = getActiveMediaController(context)
if (controller != null) {
controller.transportControls.seekTo(positionMs.coerceAtLeast(0L))
Log.d(TAG, "Seeked media to ${positionMs}ms")
true
} else {
Log.w(TAG, "No active media controller for seek")
false
}
} catch (e: Exception) {
Log.e(TAG, "Error in seekTo: ${e.message}")
false
}
}

/**
* Toggle like status by invoking the Like/Unlike action in the active media notification.
*/
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ object SyncManager {
last.artist != currentAudio.artist ||
last.volume != currentAudio.volume ||
last.isMuted != currentAudio.isMuted ||
last.isBuffering != currentAudio.isBuffering ||
last.durationMs != currentAudio.durationMs ||
kotlin.math.abs(last.positionMs - currentAudio.positionMs) >= 5_000L ||
last.likeStatus != currentAudio.likeStatus
) {
shouldSync = true
Expand Down Expand Up @@ -618,4 +621,4 @@ object SyncManager {
lastBatteryInfo = null
lastVolume = -1
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ object WebSocketMessageHandler {
}

/**
* Handles media control commands (play/pause, next, previous, like).
* Handles media control commands (play/pause, seek, next, previous, like).
* Sends a response back to Mac and updates local media state after a short delay.
*/
private fun handleMediaControl(context: Context, data: JSONObject?) {
Expand Down Expand Up @@ -243,6 +243,13 @@ object WebSocketMessageHandler {
message = if (success) "Playback paused" else "Failed to pause playback"
}

"seekTo" -> {
val positionMs = data.optLong("positionMs", -1L)
success = positionMs >= 0L && MediaControlUtil.seekTo(context, positionMs)
message =
if (success) "Seeked to ${positionMs}ms" else "Failed to seek playback"
}

"next" -> {
// Suppress automatic media updates before executing skip command
SyncManager.suppressMediaUpdatesForSkip()
Expand Down Expand Up @@ -289,14 +296,15 @@ object WebSocketMessageHandler {

// Send updated media state after successful control
if (success) {
// For track skip actions (next/previous), add a delay to allow media player to update
CoroutineScope(Dispatchers.IO).launch {
val delayMs = when (action) {
"next", "previous" -> 1200L
else -> 400L // smaller delay for like/others
}
delay(delayMs)
SyncManager.onMediaStateChanged(context)
// For track skip actions (next/previous), add a delay to allow media player to update
CoroutineScope(Dispatchers.IO).launch {
val delayMs = when (action) {
"seekTo" -> 650L
"next", "previous" -> 1200L
else -> 400L // smaller delay for like/others
}
delay(delayMs)
SyncManager.onMediaStateChanged(context)
}
}
} catch (e: Exception) {
Expand Down Expand Up @@ -915,4 +923,3 @@ object WebSocketMessageHandler {
}
}
}