I'm trying to implement the Android Media3 MediaSessionService and MediaController but for some reason the playback doesn't start. What am I doing wrong? I think I did everything exactly as described in Play media in the background.
PlaybackService.kt
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, player).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
mediaSession
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
}
MainActivity.kt
class MainActivity : ComponentActivity() {
private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var controller: MediaController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
log("onCreate MainActivity")
setContent {
TestMediaTheme {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Button(onClick = {
//val url = "android.resource://$packageName/${R.raw.test}"
val url = "https://download.samplelib.com/mp3/sample-15s.mp3"
play(url)
}) {
Text(text = "Play")
}
}
}
}
}
override fun onStart() {
super.onStart()
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
controllerFuture.addListener(
{
controller = controllerFuture.get()
initController()
},
MoreExecutors.directExecutor()
)
}
override fun onStop() {
MediaController.releaseFuture(controllerFuture)
super.onStop()
}
private fun initController() {
//controller.playWhenReady = true
controller.addListener(object : Player.Listener {
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
super.onMediaMetadataChanged(mediaMetadata)
log("onMediaMetadataChanged=$mediaMetadata")
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
log("onIsPlayingChanged=$isPlaying")
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
log("onPlaybackStateChanged=${getStateName(playbackState)}")
}
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
log("onPlayerError=${error.stackTraceToString()}")
}
override fun onPlayerErrorChanged(error: PlaybackException?) {
super.onPlayerErrorChanged(error)
log("onPlayerErrorChanged=${error?.stackTraceToString()}")
}
})
log("start=${getStateName(controller.playbackState)}")
log("COMMAND_PREPARE=${controller.isCommandAvailable(COMMAND_PREPARE)}")
log("COMMAND_SET_MEDIA_ITEM=${controller.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)}")
log("COMMAND_PLAY_PAUSE=${controller.isCommandAvailable(COMMAND_PLAY_PAUSE)}")
}
private fun play(url: String) {
log("play($url)")
log("before=${getStateName(controller.playbackState)}")
controller.setMediaItem(MediaItem.fromUri(url))
controller.prepare()
controller.play()
log("after=${getStateName(controller.playbackState)}")
}
private fun getStateName(i: Int): String? {
return when (i) {
1 -> "STATE_IDLE"
2 -> "STATE_BUFFERING"
3 -> "STATE_READY"
4 -> "STATE_ENDED"
else -> null
}
}
private fun log(message: String) {
Log.e("=====[TestMedia]=====", message)
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.TestMedia"
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="#style/Theme.TestMedia">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<service
android:name=".PlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>
And here's the debug log:
01:51:22.004 E onCreate MainActivity
01:51:22.544 E start=STATE_IDLE
01:51:22.544 E COMMAND_PREPARE=true
01:51:22.544 E COMMAND_SET_MEDIA_ITEM=true
01:51:22.544 E COMMAND_PLAY_PAUSE=true
//click 1
01:51:24.027 E play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:24.027 E before=STATE_IDLE
01:51:24.029 E onPlaybackStateChanged=STATE_BUFFERING
01:51:24.029 E after=STATE_BUFFERING
01:51:24.053 E onPlaybackStateChanged=STATE_ENDED
//click 2
01:51:25.715 E play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:25.715 E before=STATE_ENDED
01:51:25.716 E onPlaybackStateChanged=STATE_BUFFERING
01:51:25.716 E after=STATE_BUFFERING
//click 3
01:51:26.749 E play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:26.749 E before=STATE_BUFFERING
01:51:26.750 E after=STATE_BUFFERING
//click 4
01:51:30.172 E play(https://download.samplelib.com/mp3/sample-15s.mp3)
01:51:30.172 E before=STATE_BUFFERING
01:51:30.173 E after=STATE_BUFFERING
So it looks as if after the first click the player buffers and then immediately ends, and after the second click it just buffers indefinitely. Anyone got an idea what might be the problem?
Finally I found the solution thanks to this issue and this question. It seems like the Media3 Guide is missing a very crucial part.
From the onAddMediaItems documentation: Note that the requested media items don't have a MediaItem.LocalConfiguration (for example, a URI) and need to be updated to make them playable by the underlying Player.
In the end I solved it by overriding MediaSession.Callback.onAddMediaItems
class PlaybackService : MediaSessionService(), MediaSession.Callback {
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, player).setCallback(this).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
mediaSession
override fun onDestroy() {
mediaSession?.run {
player.release()
release()
mediaSession = null
}
super.onDestroy()
}
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: MutableList<MediaItem>
): ListenableFuture<MutableList<MediaItem>> {
val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
return Futures.immediateFuture(updatedMediaItems)
}
}
and then replacing
controller.setMediaItem(MediaItem.fromUri(url))
by
val media = MediaItem.Builder().setMediaId(url).build()
controller.setMediaItem(media)
Related
I have implemented music service in my spotifyclone android app but when I run project I am getting following exception
java.lang.RuntimeException: Unable to create service com.example.spotifyclone.exoplayer.MusicService: java.lang.IllegalArgumentException
at android.app.ActivityThread.handleCreateService(ActivityThread.java:3610)
at android.app.ActivityThread.access$1500(ActivityThread.java:206)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1716)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:201)
at android.app.ActivityThread.main(ActivityThread.java:6820)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:922)
Caused by: java.lang.IllegalArgumentException
at com.google.android.exoplayer2.util.Assertions.checkArgument(Assertions.java:39)
at com.google.android.exoplayer2.ui.PlayerNotificationManager$Builder.<init>(PlayerNotificationManager.java:353)
at com.example.spotifyclone.exoplayer.MusicNotificationManager.<init>(MusicNotificationManager.kt:33)
at com.example.spotifyclone.exoplayer.MusicService.onCreate(MusicService.kt:71)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:3598)
below my MusicService.kt
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import androidx.media.MediaBrowserServiceCompat
import com.example.spotifyclone.exoplayer.callbacks.MusicPlaybackPreparer
import com.example.spotifyclone.exoplayer.callbacks.MusicPlayerEventListener
import com.example.spotifyclone.exoplayer.callbacks.MusicPlayerNotificationListener
import com.example.spotifyclone.other.Constants.MEDIA_ROOT_ID
import com.example.spotifyclone.other.Constants.NETWORK_ERROR
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import javax.inject.Inject
private const val SERVICE_TAG = "MusicService"
#AndroidEntryPoint
class MusicService : MediaBrowserServiceCompat() {
#Inject
lateinit var dataSourceFactory: DefaultDataSourceFactory
#Inject
lateinit var exoPlayer: ExoPlayer
#Inject
lateinit var firebaseMusicSource: FirebaseMusicSource
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaSessionConnector: MediaSessionConnector
var isForegroundService = false
private var curPlayingSong: MediaMetadataCompat? = null
private lateinit var musicNotificationManager: MusicNotificationManager
private var isPlayerInitialized = false
private lateinit var musicPlayerEventListener: MusicPlayerEventListener
companion object {
var curSongDuration = 0L
private set
}
override fun onCreate() {
super.onCreate()
serviceScope.launch {
firebaseMusicSource.fetchMediaData()
}
val activityIntent = packageManager?.getLaunchIntentForPackage(packageName)?.let {
PendingIntent.getActivity(this, 0, it, 0)
}
mediaSession = MediaSessionCompat(this, SERVICE_TAG).apply {
setSessionActivity(activityIntent)
isActive = true
}
sessionToken = mediaSession.sessionToken
musicNotificationManager = MusicNotificationManager(
this,
mediaSession.sessionToken,
MusicPlayerNotificationListener(this)
) {
curSongDuration = exoPlayer.duration
}
val musicPlaybackPreparer = MusicPlaybackPreparer(firebaseMusicSource) {
curPlayingSong = it
preparePlayer(
firebaseMusicSource.songs,
it,
true
)
}
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlaybackPreparer(musicPlaybackPreparer)
mediaSessionConnector.setQueueNavigator(MusicQueueNavigator())
mediaSessionConnector.setPlayer(exoPlayer)
musicPlayerEventListener = MusicPlayerEventListener(this)
exoPlayer.addListener(musicPlayerEventListener)
musicNotificationManager.showNotification(exoPlayer)
}
private inner class MusicQueueNavigator : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
return firebaseMusicSource.songs[windowIndex].description
}
}
private fun preparePlayer(
songs: List<MediaMetadataCompat>,
itemToPlay: MediaMetadataCompat?,
playNow: Boolean
) {
val curSongIndex = if (curPlayingSong == null) 0 else songs.indexOf(itemToPlay)
exoPlayer.setMediaSource(firebaseMusicSource.asMediaSource(dataSourceFactory))
exoPlayer.prepare()
exoPlayer.seekTo(curSongIndex, 0L)
exoPlayer.playWhenReady = playNow
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
exoPlayer.stop()
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
exoPlayer.removeListener(musicPlayerEventListener)
exoPlayer.release()
}
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
return BrowserRoot(MEDIA_ROOT_ID, null)
}
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
when (parentId) {
MEDIA_ROOT_ID -> {
val resultsSent = firebaseMusicSource.whenReady { isInitialized ->
if (isPlayerInitialized) {
result.sendResult(firebaseMusicSource.asMediaItems())
if (!isInitialized && firebaseMusicSource.songs.isNotEmpty()) {
preparePlayer(
firebaseMusicSource.songs,
firebaseMusicSource.songs[0],
false
)
isPlayerInitialized = true
}
} else {
mediaSession.sendSessionEvent(NETWORK_ERROR, null)
result.sendResult(null)
}
}
if (!resultsSent) {
result.detach()
}
}
}
}
}
below my AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.spotifyclone">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".SpotifyApplication"
android:allowBackup="true"
android:dataExtractionRules="#xml/data_extraction_rules"
android:fullBackupContent="#xml/backup_rules"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.SpotifyClone"
tools:targetApi="31">
<service android:name="com.example.spotifyclone.exoplayer.MusicService"
android:exported="false">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<activity
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
below MusicNotificationManager.kt
#SuppressLint("Range")
class MusicNotificationManager(
private val context:Context,
sessionToken:MediaSessionCompat.Token,
notificationListener:PlayerNotificationManager.NotificationListener,
private val newSongCallback:() -> Unit
) {
private val notificationManager: PlayerNotificationManager
init {
val mediaController = MediaControllerCompat(context, sessionToken)
notificationManager = PlayerNotificationManager.Builder(
context,
NOTIFICATION_ID, NOTIFICATION_CHANNEL_ID
)
.setChannelNameResourceId(R.string.notification_channel_name)
.setChannelDescriptionResourceId(R.string.notification_channel_description)
.setMediaDescriptionAdapter(DescriptionAdapter(mediaController))
.setNotificationListener(notificationListener)
.setSmallIconResourceId(R.drawable.ic_music)
.build()
}
fun showNotification(player: Player) {
notificationManager.setPlayer(player)
}
private inner class DescriptionAdapter(
private val mediaController: MediaControllerCompat
) : PlayerNotificationManager.MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): CharSequence {
return mediaController.metadata.description.title.toString()
}
override fun createCurrentContentIntent(player: Player): PendingIntent? {
return mediaController.sessionActivity
}
override fun getCurrentContentText(player: Player): CharSequence? {
return mediaController.metadata.description.subtitle.toString()
}
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
Glide.with(context).asBitmap()
.load(mediaController.metadata.description.iconUri)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
callback.onBitmap(resource)
}
override fun onLoadCleared(placeholder: Drawable?) = Unit
})
return null
}
}
}
what I have done rebuild invalidate cache and restart and followed allstackoverflow answers
I want to know where exactly I am making mistake what I have to do avoid crash in my app
The stacktrace mentions the line 353 in the PlayerNotificationManager source code:
at com.google.android.exoplayer2.ui.PlayerNotificationManager$Builder.<init>(PlayerNotificationManager.java:353)
Here is exactly that line (You can find the source code on github):
checkArgument(notificationId > 0);
As You can see, the library checks if the NOTIFICATION_ID that You're passing is greater than 0. The solution is to change the definition of NOTIFICATION_ID to an integer greater than 0. Good luck with the projet :)
I`m trying to build a service that detects if the screen of a device is locked/unlocked (which I will later user as a native module in React). However, it seems like my service is not starting, and I don't receive the expected logs. Where is my mistake? (It's my first time dealing with native android & Kotlin, so apologies if this is a dumb question, and duplicates were related to java code)..
I have defined a Broadcast Receiver for each event here:
ScreenOnReceiver.kt
class screenOnReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if(intent!!.action == Intent.ACTION_SCREEN_ON) {
val screenOff = false
val i = Intent(context, PowerButtonService::class.java)
i.putExtra("screenState", screenOff)
context!!.startService(i)
}
}
}
ScreenOffReceiver.kt
class screenOffReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if(intent!!.action == Intent.ACTION_SCREEN_ON) {
val screenOff = false
val i = Intent(context, PowerButtonService::class.java)
i.putExtra("screenState", screenOff)
context!!.startService(i)
}
}
}
ScreenChangeService.kt:
class ScreenChangeService: Service() {
override fun onBind(p0: Intent?): IBinder? = null
override fun onCreate() {
val screenOnReceiver = screenONReceiver()
val screenOnFilter = IntentFilter(Intent.ACTION_SCREEN_ON)
registerReceiver(screenOnReceiver, screenOnFilter)
val screenOffReceiver = screenOffReceiver()
val screenOffFilter = IntentFilter(Intent.ACTION_SCREEN_OFF)
registerReceiver(screenOffreceiver, screenOffFilter)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val screenState = intent!!.getBooleanExtra("screenState", false)
if (screenState == true) {
Log.d("TAG", "Screen On")
} else {
Log.d("TAG", "Screen Off")
}
return START_NOT_STICKY
}
}
Manifest.xml
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.Test">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".PowerButtonService"/>
</application>
I creating softphone application using Android SIP API (https://developer.android.com/guide/topics/connectivity/sip) and I have successfully registered to my Asterisk server and I can call another peer on another softphone. I can hear voice on both sides and everything works perfect.
Now, I have problem receiving call from another softphone. onRinging method is never executed. On the other softphone I am getting status RINGING, but in my app that call is never received.
I have created IncomingCallReceiver class and also WalkieTalkieActivity, but none of them is doing its job (most probably I am doing something wrong, or I did not initiate some of them, or call or whatever...).
I am stuck on receiving the calls. Also the official documentation is not clear to me.
If there is anyone here to help me and guide me to the solution, I would be very grateful.
Here is my MainActivity class:
class MainActivity: FlutterActivity() {
var mSipProfile: SipProfile? = null
var username: String? = "..." //I have it correct in my code
var password: String? = "..." //I have it correct in my code
var domain: String? = "..." //I have it correct in my code
var builder: SipProfile.Builder? = SipProfile.Builder(username, domain).setPassword(password)
private val CHANNEL = "samples.flutter.dev/registration"
private val mSipManager: SipManager? by lazy(LazyThreadSafetyMode.NONE) {
SipManager.newInstance(this)
}
override fun onStart() {
super.onStart()
println("SSSSSTTTTTTTTAAAAAAAAAAARRRRRRRRRRRRRTTTTTTTTTTTTTTTTTTT")
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
println("CCCCCCCCRRRRRREEEEEEEEEAAAAAAATTTTTTTTEEEEEEEEEEEEEEEEEEEEEE")
}
private var listenSipSession: SipSession.Listener = object : SipSession.Listener() {
override fun onRinging(session: SipSession?, caller: SipProfile?, sessionDescription: String?) {
super.onRinging(session, caller, sessionDescription)
println("EVVVVVVVOOOOOOOOOOOOOOOO GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
}
}
private var listener: SipAudioCall.Listener = object : SipAudioCall.Listener() {
override fun onCalling(call: SipAudioCall?) {
super.onCalling(call)
println("111111 CALLING CALLING CALLING")
}
override fun onChanged(call: SipAudioCall?) {
super.onChanged(call)
println("CHANGED")
println(call?.state)
}
override fun onError(call: SipAudioCall?, errorCode: Int, errorMessage: String?) {
super.onError(call, errorCode, errorMessage)
println("ERROR ERROR ERROR")
}
override fun onRingingBack(call: SipAudioCall?) {
super.onRingingBack(call)
println("RINGING BACK")
}
/*override fun onRinging(call: SipAudioCall, caller: SipProfile) {
println("RINGING")
try {
call.answerCall(30)
} catch (e: Exception) {
println("ERROR 2: $e")
e.printStackTrace()
}
}*/
override fun onCallEstablished(call: SipAudioCall) {
println("Call established")
call.apply {
startAudio()
setSpeakerMode(false)
// toggleMute()
}
}
override fun onRinging(call: SipAudioCall?, caller: SipProfile?) {
super.onRinging(call, caller)
println("Ringing")
}
override fun onCallEnded(call: SipAudioCall) {
println("Call ended " + call.state)
}
}
override fun onDestroy() {
super.onDestroy()
closeLocalProfile()
}
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
// Note: this method is invoked on the main thread.
mSipProfile = builder?.build()
val intent = Intent("com.example.softphone.INCOMING_CALL")
val pendingIntent: PendingIntent = PendingIntent.getBroadcast(this, 0, intent, Intent.FILL_IN_DATA)
mSipManager?.open(mSipProfile, pendingIntent, null)
if (call.method == "register") {
val registrationStatus = register()
result.success(registrationStatus)
}
if (call.method == "call") {
call()
}
}
}
private fun closeLocalProfile() {
try {
mSipManager?.close(mSipProfile?.uriString)
println("Local profile closed.")
} catch (ee: Exception) {
println("WalkieTalkieActivity/onDestroy. Failed to close local profile.")
}
}
///Method for making call
private fun call(): String {
var status = "Unknown"
try {
println("Making call")
status = "In Call"
val call: SipAudioCall? = mSipManager?.makeAudioCall(
mSipProfile?.uriString,
"...", //I have it correct in my code
listener,
30
)
println(call)
} catch (e: SipException) {
e.printStackTrace()
println(e)
}
return status
}
///Method for registering user
private fun register(): String {
var status = "Unknown"
try {
mSipManager?.setRegistrationListener(mSipProfile?.uriString, object : SipRegistrationListener {
override fun onRegistering(localProfileUri: String) {
status = "Registering with SIP Server..."
println(status)
}
override fun onRegistrationDone(localProfileUri: String, expiryTime: Long) {
status = "Ready"
println(status)
}
override fun onRegistrationFailed(
localProfileUri: String,
errorCode: Int,
errorMessage: String
) {
status = "Registration failed. Please check settings."
println(status)
}
})
} catch (e: SipException) {
e.printStackTrace()
println(e)
}
return status
}
}
Here is my IncomingCallReceiver class:
class IncomingCallReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
var incomingCall: SipAudioCall? = null
try {
val listener: SipAudioCall.Listener = object : SipAudioCall.Listener() {
override fun onRinging(call: SipAudioCall, caller: SipProfile) {
try {
call.answerCall(30)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
val wtActivity: WalkieTalkieActivity = context as WalkieTalkieActivity
incomingCall = wtActivity.manager?.takeAudioCall(intent, listener)
incomingCall?.setListener(listener)
incomingCall?.answerCall(30)
incomingCall?.startAudio()
incomingCall?.setSpeakerMode(true)
if (incomingCall?.isMuted!!) {
incomingCall?.toggleMute()
}
} catch (e: Exception) {
incomingCall?.close()
}
}
}
Here is my WalkieTalkieActivity class:
class WalkieTalkieActivity : Activity(), OnTouchListener {
var sipAddress: String? = null
var manager: SipManager? = null
var me: SipProfile? = null
var call: SipAudioCall? = null
lateinit var callReceiver: IncomingCallReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val filter = IntentFilter().apply {
addAction("com.example.softphone.INCOMING_CALL")
}
callReceiver = IncomingCallReceiver()
this.registerReceiver(callReceiver, filter)
}
override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
}
Here is my AndroidManifest file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.softphone">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.USE_SIP" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-feature android:name="android.software.sip.voip" android:required="true" />
<uses-feature android:name="android.hardware.wifi" android:required="true" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<application
android:name="io.flutter.app.FlutterApplication"
android:label="softphone"
android:icon="#mipmap/ic_launcher">
<receiver android:name=".IncomingCallReceiver" android:label="Call Receiver" />
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="#style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="#style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="#drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
Thank you all in advance!
I'm trying to receive media button events from Wired/Bluetooth headsets
I'm receiving media button events on onMediaButtonEvent(mediaButtonEvent: Intent?) method in MediaSessionCallback
but nothing happens to the music playback.
Here is my MediaPlayerService
private const val LOG_TAG = "LOG_TAG"
private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"
private const val AUDIO_URL_1 = "https://www.listennotes.com/e/p/94051189e660408b861be9ee28f17f06/"
class MediaPlaybackService : MediaBrowserServiceCompat() {
private val TAG = "MediaPlaybackService"
private lateinit var context: Context
private lateinit var mediaSession: MediaSessionCompat
private lateinit var stateBuilder: PlaybackStateCompat.Builder
private lateinit var exoPlayer: SimpleExoPlayer
private lateinit var dataSourceFactory: DefaultDataSourceFactory
private val audioAttributes = AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build()
override fun onCreate() {
super.onCreate()
context = this
initExoPlayer()
initDataSourceFactory()
initMediaSession()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand ${intent?.getParcelableExtra<KeyEvent>
(Intent.EXTRA_KEY_EVENT)?.keyCode}")
MediaButtonReceiver.handleIntent(mediaSession, intent)
return super.onStartCommand(intent, flags, startId)
}
private fun initExoPlayer() {
exoPlayer = ExoPlayerFactory.newSimpleInstance(context)
exoPlayer.setAudioAttributes(audioAttributes, true)
}
private fun initDataSourceFactory() {
val httpDataSourceFactory = DefaultHttpDataSourceFactory(
Util.getUserAgent(context, "media-player"),
null,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true
)
dataSourceFactory = DefaultDataSourceFactory(context, null, httpDataSourceFactory)
}
private fun initMediaSession() {
mediaSession = MediaSessionCompat(context, LOG_TAG).apply {
setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
stateBuilder = PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
setPlaybackState(stateBuilder.build())
setCallback(mediaSessionCallback())
setSessionToken(sessionToken)
isActive = true
}
}
private fun mediaSessionCallback() = object : MediaSessionCompat.Callback() {
override fun onPlay() {
super.onPlay()
play()
}
override fun onPause() {
super.onPause()
pause()
}
override fun onSkipToNext() {
super.onSkipToNext()
skipToNext()
}
override fun onSkipToPrevious() {
super.onSkipToPrevious()
skipToPrevious()
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
Log.i(TAG, "MediaButtonEvent: ${mediaButtonEvent?.getParcelableExtra<KeyEvent>
(Intent.EXTRA_KEY_EVENT)?.keyCode}")
return super.onMediaButtonEvent(mediaButtonEvent)
}
}
fun play() {
Log.i(TAG, "Playback State: Playing")
if (mediaSession.controller.playbackState.state != PlaybackStateCompat.STATE_PAUSED) {
val mediaSource = getMediaSource(AUDIO_URL_1)
exoPlayer.prepare(mediaSource)
}
setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
exoPlayer.playWhenReady = true
setMediaMetadata(title = "Episode 131: Bourne Wild")
}
fun pause() {
Log.i(TAG, "Playback State: Paused")
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
exoPlayer.playWhenReady = false
}
fun skipToNext() {
if (exoPlayer.hasNext()) {
Log.i(TAG, "ExoPLayer: Skip to Next")
exoPlayer.next()
}
}
fun skipToPrevious() {
if (exoPlayer.hasPrevious()) {
Log.i(TAG, "ExoPLayer: Skip to Previous")
exoPlayer.previous()
}
}
override fun onLoadChildren(parentId: String, result:
Result<MutableList<MediaBrowserCompat.MediaItem>>) {
result.sendResult(null)
}
override fun onGetRoot(lientPackageName: String, clientUid: Int,
rootHints: Bundle?): BrowserRoot? {
return BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null)
}
override fun onDestroy() {
exoPlayer.release()
mediaSession.run {
isActive = false
release()
}
super.onDestroy()
}
}
Here is my Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.media_player">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:usesCleartextTraffic="true"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application>
</manifest>
It looks like I'm missing something, after many days of struggling, I couldn't find what's wrong.
Any Help will be very much appreciated
Thanks
I'm building a simple app, that keep monitoring the media level, and adjust it to be 20% of the maximum level all the time, if the user increased,it should back to 20%again.
Th concept I followed is doing the monitoring process via a service, once this service is destroyed it calls a broadcast receiver, which in its turn calls the receiver again, and so on, as endless cycle, but looks something wrong in the code below,soit is not working as desired, and service/broadcast not keep calling each others!
I started the mainActivity as:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val audio = this.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val percent = 0.2f
val twintyVolume = (maxVolume * percent).toInt()
if ( level > twintyVolume) {
Toast.makeText(this,"audio level is $level", Toast.LENGTH_LONG).show()
audio.setStreamVolume(AudioManager.STREAM_MUSIC,twintyVolume,0)
}
this.startService(Intent(this, VolumeCheck::class.java))
}
}
The above make initial check and reduce the media volume to 20% of the max volume, then start the service, which is doing the same with the below code:
class VolumeCheck : Service() {
private lateinit var context: Context
override fun onCreate() {
super.onCreate()
context = this
Toast.makeText(this, "service created", Toast.LENGTH_SHORT).show();
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
val audio = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val mediaPlayer = MediaPlayer()
// Thread().run {
val maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val percent = 0.2f
val twintyVolume = (maxVolume * percent).toInt()
if (mediaPlayer.isPlaying) {
val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
if ( level > twintyVolume) {
Toast.makeText(context,"audio level is $level", Toast.LENGTH_LONG).show()
audio.setStreamVolume(AudioManager.STREAM_MUSIC,twintyVolume,0)
}
}
// Thread.sleep(3000)
// }
stopSelf()
return Service.START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
//TODO for communication return IBinder implementation
return null
}
override fun onDestroy() {
super.onDestroy()
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
val intent = Intent("com.kortex.mediafix.BootUpReceiver")
sendBroadcast(intent)
}
}
Once the service is destroyed, it calls the boot broadcast receiver, which in its turn call the service again:
class BootUpReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.startService(Intent(context, VolumeCheck::class.java))
}
}
The Manifest is:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kortex.mediafix">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".BootUpReceiver"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<service android:name=".VolumeCheck" />
</application>
</manifest>
val intent = Intent("com.kortex.mediafix.BootUpReceiver")
This line does not call your broadcast receiver but rather makes an intent with intent action as "com.kortex.mediafix.BootUpReceiver"
Change your BootUpReceiver's entry in manifest to receive this action
<receiver android:name=".BootUpReceiver"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.kortex.mediafix.BootUpReceiver" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
Another way to do it, without Service is to call ContentObserver from BroadcastReceiver
BroadcastReceiver:
class BootUpReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val myObserver = VolumeOnserver(context, Handler())
// Register the VolumeOnserver for setting changes
context.contentResolver.registerContentObserver(
android.provider.Settings.System.CONTENT_URI ,true,
myObserver)
}
}
ContentObserver:
class VolumeOnserver (context: Context, h: Handler?): ContentObserver(h) {
private val context = context
override fun onChange(selfChange: Boolean) {
onChange(selfChange, uri = null)
}
// Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
override fun onChange(selfChange: Boolean, uri: Uri?) {
// Handle change.
val audio = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val percent = 0.2f
val twintyVolume = (maxVolume * percent).toInt()
if ( level > twintyVolume) audio.setStreamVolume(AudioManager.STREAM_MUSIC,twintyVolume,0)
}
}
MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val audio = this.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val percent = 0.2f
val twintyVolume = (maxVolume * percent).toInt()
if ( level > twintyVolume) audio.setStreamVolume(AudioManager.STREAM_MUSIC,twintyVolume,0)
finish()
}
}
I solved it by using ContentObserver that is called from the service, so my code now is:
MainActivity to launch the app, and do first time adjustment, and to start the service:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val audio = this.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val percent = 0.2f
val twintyVolume = (maxVolume * percent).toInt()
if ( level > twintyVolume) audio.setStreamVolume(AudioManager.STREAM_MUSIC,twintyVolume,0)
Toast.makeText(this,"Audio level adjusted to 20% instead of $level", Toast.LENGTH_LONG).show()
this.startService(Intent(this, VolumeCheck::class.java))
finish()
}
}
Broadcast to start the app and the service each time the device is rebooted:
class BootUpReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
context.startService(Intent(context, VolumeCheck::class.java))
}
}
Service to register and call the observer:
class VolumeCheck : Service() {
private lateinit var context: Context
private lateinit var myObserver: VolumeOnserver
override fun onCreate() {
super.onCreate()
context = this
// Define the VolumeOnserver
myObserver = VolumeOnserver(context, Handler())
Toast.makeText(this, "service created", Toast.LENGTH_SHORT).show();
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
// Register the VolumeOnserver for setting changes
contentResolver.registerContentObserver(android.provider.Settings.System.CONTENT_URI ,true, myObserver);
return Service.START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
// Unregister the VolumeOnserver
contentResolver.unregisterContentObserver(myObserver);
}
}
Observer that observe any changes in the settings and check the media volume and adjust it if required:
class VolumeOnserver (context: Context, h: Handler?): ContentObserver(h) {
private val context = context
override fun onChange(selfChange: Boolean) {
onChange(selfChange, uri = null)
}
// Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
override fun onChange(selfChange: Boolean, uri: Uri?) {
// Handle change.
val audio = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val level = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
val maxVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val percent = 0.2f
val twintyVolume = (maxVolume * percent).toInt()
if ( level > twintyVolume) audio.setStreamVolume(AudioManager.STREAM_MUSIC,twintyVolume,0)
// Toast.makeText(context,"audio level is $level", Toast.LENGTH_LONG).show()
}
}
The Manifest file is:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kortex.mediafix">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".BootUpReceiver"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.kortex.mediafix.BootUpReceiver" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<service android:name=".VolumeCheck" />
</application>
</manifest>
And my running app is here.
But have an issue it is not stable, I the user insist to change the volume, after many trials the service and/or the observer is/are no more working till the app is restarted.