- Replace legacy MediaBrowserServiceCompat/MediaSessionCompat with
Media3 MediaSessionService (MusicService.kt) + ExoPlayer set to
WAKE_MODE_NETWORK — holds WakeLock + WifiLock through Doze so audio
plays uninterrupted on locked screen
- Media3 manages the notification; no manual NotificationCompat.Builder
→ no disappear/reappear flicker when skipping tracks
- Full bidirectional bridge:
JS → native: window.NSBridge.* (WebBridge.kt, 13 methods)
Native → JS: window.NSPlayer.* callbacks (onReady, onTrackChanged,
onPositionUpdate, onPlaybackStateChanged, onQueueEnded, onError)
- player.js: progressive enhancement — all playback routes through
NSBridge when running inside the wrapper, falls back to S.audio on
plain PWA/browser; removes all window.Android.* calls
- main.js: guard _onForeground mediaSession playbackState setter behind
!window.NSBridge so native Media3 session is never stomped
- ProGuard rules updated to keep WebBridge @JavascriptInterface methods
and Media3 classes
- bridge-test.html: manual integration test page for every bridge method
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
54 lines
1.6 KiB
Kotlin
54 lines
1.6 KiB
Kotlin
package com.novasonic.app
|
|
|
|
import android.net.Uri
|
|
import androidx.media3.common.MediaItem
|
|
import androidx.media3.common.MediaMetadata
|
|
import org.json.JSONArray
|
|
import org.json.JSONObject
|
|
|
|
data class Track(
|
|
val id: String,
|
|
val url: String,
|
|
val title: String,
|
|
val artist: String,
|
|
val album: String?,
|
|
val artworkUrl: String?,
|
|
val durationMs: Long
|
|
) {
|
|
fun toMediaItem(): MediaItem {
|
|
val metadata = MediaMetadata.Builder()
|
|
.setTitle(title)
|
|
.setArtist(artist)
|
|
.setAlbumTitle(album)
|
|
.apply {
|
|
if (!artworkUrl.isNullOrEmpty()) setArtworkUri(Uri.parse(artworkUrl))
|
|
if (durationMs > 0) setDurationMs(durationMs)
|
|
}
|
|
.build()
|
|
return MediaItem.Builder()
|
|
.setMediaId(id)
|
|
.setUri(url)
|
|
.setMediaMetadata(metadata)
|
|
.build()
|
|
}
|
|
|
|
companion object {
|
|
fun fromJson(obj: JSONObject) = Track(
|
|
id = obj.optString("id", ""),
|
|
url = obj.optString("url", ""),
|
|
title = obj.optString("title", ""),
|
|
artist = obj.optString("artist", ""),
|
|
album = obj.optString("album").takeIf { it.isNotEmpty() },
|
|
artworkUrl = obj.optString("artworkUrl").takeIf { it.isNotEmpty() },
|
|
durationMs = obj.optLong("durationMs", 0)
|
|
)
|
|
}
|
|
}
|
|
|
|
fun parseTrackList(json: String): List<Track> = try {
|
|
val arr = JSONArray(json)
|
|
List(arr.length()) { Track.fromJson(arr.getJSONObject(it)) }
|
|
} catch (_: Exception) {
|
|
emptyList()
|
|
}
|