Skip to content
Open
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: 2 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<!-- Media and audio permissions -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

<!-- Storage permissions for local music files -->
<!-- For Android 12 and below -->
Expand Down Expand Up @@ -85,7 +86,7 @@
android:name=".MusicService"
android:exported="true"
android:enabled="true"
android:stopWithTask="true"
android:stopWithTask="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
Expand Down
47 changes: 34 additions & 13 deletions android/app/src/main/kotlin/com/musly/musly/AndroidAutoPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ package com.devid.musly

import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel

object AndroidAutoPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {


private const val TAG = "AndroidAutoPlugin"
private const val METHOD_CHANNEL = "com.devid.musly/android_auto"
private const val EVENT_CHANNEL = "com.devid.musly/android_auto_events"

private var methodChannel: MethodChannel? = null
private var eventChannel: EventChannel? = null
private var eventSink: EventChannel.EventSink? = null
private var context: Context? = null
private val mainHandler = Handler(Looper.getMainLooper())

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
Expand All @@ -29,13 +34,13 @@ object AndroidAutoPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}

override fun onCancel(arguments: Any?) {
eventSink = null
}
})
startMusicService()

Log.d(TAG, "AndroidAutoPlugin attached (service will start on first playback)")
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
Expand Down Expand Up @@ -65,10 +70,21 @@ object AndroidAutoPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
val position = call.argument<Number>("position")?.toLong() ?: 0L
val playing = call.argument<Boolean>("playing") ?: false

MusicService.getInstance()?.updatePlaybackState(
songId, title, artist, album, artworkUrl, duration, position, playing
)

// Ensure the service is running before updating state
val pushState = {
MusicService.getInstance()?.updatePlaybackState(
songId, title, artist, album, artworkUrl, duration, position, playing
)
}
if (MusicService.getInstance() == null) {
Log.d(TAG, "MusicService not running, starting it now")
startMusicService()
// Service start is async; retry after a short delay
mainHandler.postDelayed({ pushState() }, 200)
} else {
pushState()
}
result.success(null)
}
"updateRecentSongs" -> {
Expand Down Expand Up @@ -113,11 +129,16 @@ object AndroidAutoPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}

private fun startMusicService() {
fun startMusicService() {
context?.let { ctx ->
val intent = Intent(ctx, MusicService::class.java)
ContextCompat.startForegroundService(ctx, intent)
}
try {
val intent = Intent(ctx, MusicService::class.java)
ContextCompat.startForegroundService(ctx, intent)
Log.d(TAG, "startForegroundService called successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to start MusicService: ${e.message}", e)
}
} ?: Log.w(TAG, "Cannot start MusicService: context is null")
}

private fun stopMusicService() {
Expand Down
30 changes: 26 additions & 4 deletions android/app/src/main/kotlin/com/musly/musly/AndroidSystemPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
Expand Down Expand Up @@ -122,10 +123,20 @@ object AndroidSystemPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
val duration = call.argument<Number>("duration")?.toLong() ?: 0L
val position = call.argument<Number>("position")?.toLong() ?: 0L
val playing = call.argument<Boolean>("playing") ?: false

MusicService.getInstance()?.updatePlaybackState(
songId, title, artist, album, artworkUrl, duration, position, playing
)

// Ensure the service is running before updating state
val pushState = {
MusicService.getInstance()?.updatePlaybackState(
songId, title, artist, album, artworkUrl, duration, position, playing
)
}
if (MusicService.getInstance() == null) {
Log.d(TAG, "MusicService not running, requesting start via AndroidAutoPlugin")
AndroidAutoPlugin.startMusicService()
handler.postDelayed({ pushState() }, 200)
} else {
pushState()
}
result.success(null)
}
"setNotificationColor" -> {
Expand Down Expand Up @@ -156,6 +167,17 @@ object AndroidSystemPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"getAndroidSdkVersion" -> {
result.success(Build.VERSION.SDK_INT)
}
"setRemotePlayback" -> {
val isRemote = call.argument<Boolean>("isRemote") ?: false
val volume = call.argument<Int>("volume") ?: 50
MusicService.getInstance()?.setRemoteVolume(isRemote, volume)
result.success(null)
}
"updateRemoteVolume" -> {
val volume = call.argument<Int>("volume") ?: 50
MusicService.getInstance()?.updateRemoteVolume(volume)
result.success(null)
}
"dispose" -> {
dispose()
result.success(null)
Expand Down
49 changes: 44 additions & 5 deletions android/app/src/main/kotlin/com/musly/musly/MusicService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import android.media.AudioManager
import android.util.Log
import androidx.media.VolumeProviderCompat
import kotlinx.coroutines.*
import java.net.URL

class MusicService : MediaBrowserServiceCompat() {

companion object {
private const val TAG = "MusicService"
private const val CHANNEL_ID = "musly_music_channel"
private const val NOTIFICATION_ID = 1
private const val MY_MEDIA_ROOT_ID = "media_root_id"
Expand Down Expand Up @@ -54,7 +58,8 @@ class MusicService : MediaBrowserServiceCompat() {
private var currentDuration: Long = 0
private var currentPosition: Long = 0
private var isPlaying: Boolean = false

private var volumeProvider: VolumeProviderCompat? = null

private val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
private val recentSongs = mutableListOf<MediaBrowserCompat.MediaItem>()
private val albums = mutableListOf<MediaBrowserCompat.MediaItem>()
Expand All @@ -72,16 +77,18 @@ class MusicService : MediaBrowserServiceCompat() {
override fun onCreate() {
super.onCreate()
instance = this

Log.d(TAG, "MusicService onCreate")

createNotificationChannel()
initializeMediaSession()

showIdleNotification()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "MusicService onStartCommand action=${intent?.action}")
MediaButtonReceiver.handleIntent(mediaSession, intent)
return START_STICKY
return START_NOT_STICKY
}

private fun showIdleNotification() {
Expand Down Expand Up @@ -633,14 +640,46 @@ class MusicService : MediaBrowserServiceCompat() {
}
}

fun setRemoteVolume(isRemote: Boolean, currentVolume: Int) {
if (isRemote) {
volumeProvider = object : VolumeProviderCompat(
VOLUME_CONTROL_ABSOLUTE, 100, currentVolume
) {
override fun onSetVolumeTo(volume: Int) {
setCurrentVolume(volume)
AndroidAutoPlugin.sendCommand("setVolume", mapOf("volume" to volume))
}

override fun onAdjustVolume(direction: Int) {
val newVolume = (currentVolume + direction * 5).coerceIn(0, 100)
setCurrentVolume(newVolume)
AndroidAutoPlugin.sendCommand("setVolume", mapOf("volume" to newVolume))
}
}
mediaSession.setPlaybackToRemote(volumeProvider!!)
Log.d(TAG, "MediaSession set to remote volume (current=$currentVolume)")
} else {
volumeProvider = null
mediaSession.setPlaybackToLocal(AudioManager.STREAM_MUSIC)
Log.d(TAG, "MediaSession set to local volume")
}
}

fun updateRemoteVolume(volume: Int) {
volumeProvider?.currentVolume = volume
}

override fun onDestroy() {
Log.d(TAG, "MusicService onDestroy")
instance = null
super.onDestroy()
serviceScope.cancel()
mediaSession.isActive = false
mediaSession.release()
}

override fun onTaskRemoved(rootIntent: Intent?) {
Log.d(TAG, "MusicService onTaskRemoved")
super.onTaskRemoved(rootIntent)
mediaSession.isActive = false
stopForeground(true)
Expand Down
Loading