In an Android app made to play some audio file in the background, I have the following situation. The app plays the audio as I expect, also keeping playing in the background. The issue I am facing is when I want to stop the service.
I have two buttons on the main activity, one for starting the service and the other one to stop it.
This is the function fired by the START button:
fun startHandler(view: View) {
val audioName = "myAudio"
val serviceIntent = Intent(this, TheService::class.java)
serviceIntent.putExtra("name", audioName);
startForegroundService(serviceIntent)
}
This is the function fired by the STOP button:
fun stopHandler(view: View) {
val serviceIntent = Intent(this, TheService::class.java)
stopService(serviceIntent)
}
When I tap the STOP button, the audio stops (as expected), but the app crashes right after (this is not expected).
Here is the onDestroy() function on the service side:
override fun onDestroy() {
super.onDestroy()
audioPlayer.stop()
}
And the onStartCommand() function on the service side:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val runnable = Runnable {
val audioName = intent?.getStringExtra("name")
val audioID = resources.getIdentifier(audioName,"raw", packageName)
audioPlayer = MediaPlayer.create(this, audioID)
audioPlayer?.setLooping(true)
audioPlayer.start()
}
val thread = Thread(runnable)
thread.start()
return START_NOT_STICKY
}
Is there any mistake that can be seen in the code above or some place I should look at in order to solve the problem ?
I use Handler for creating a timer in a Widget.
I use the recommended constructor, i.e. passing a Looper to it.
private val updateHandler = Handler(Looper.getMainLooper())
#RequiresApi(Build.VERSION_CODES.Q)
private val runnable = Runnable {
updateDisplay()
}
#RequiresApi(Build.VERSION_CODES.Q)
private fun updateDisplay () {
updateHandler?.postDelayed(runnable, TIMER_MS)
// some other code
}
The TIMER MS is set to 3000 ms.
The timer runs fine for a while and execute the given code. However after a random time elapsed the timer stops working and no more execution of the given code happens.
Please advise what the problem could be ond how to fix it.
Alternatively, can I use some other timer? (The timer should go off every few second - this is the reason why I use Handler)
Thank you for any advice in advance
You could always try using a Coroutine for something like this:
class TimedRepeater(var delayMs: Long,
var worker: (() -> Unit)) {
private var timerJob: Job? = null
suspend fun start() {
if (timerJob != null) throw IllegalStateException()
timerJob = launch {
while(isActive) {
delay(delayMs)
worker()
}
}
}
suspend fun stop() {
if (timerJob == null) return
timerJob.cancelAndJoin()
timerJob = null
}
}
suspend fun myStuff() {
val timer = Timer(1000) {
// Do my work
}
timer.start()
// Some time later
timer.stop()
}
I haven't tested the above, but it should work well enough.
You can use CountDownTimer from Android framework to achieve the same. It internally uses Handler for timer
val timer = object: CountDownTimer(1000,1000){
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
}
}
timer.start()
I need some help to achieve something that is maybe simple. My code is like this :
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
repeatAction()
val stoprepeat = findViewById(R.id.btn) as Button
stoprepeat.setOnClickListener{
// I need to completely stop the actions that are repeating from here
}
}
private fun repeatAction(){
var repeataction = FixedRateTimer("mytimer",false,2400,20000){
this#HomeActivity.runOnUiThread {
// Stuff to repeat (3 or 4 actions)
}
}
}
}
What I'm trying to do is to stop the FixedTimeRate tasks when I Click on the button.
Also, is there a way to prevent fixedRateTimer from crashing the app after maybe 10 minutes of running?
how about intruducing a var keepRunning: Boolean = true in HomeActivity that is checked every cycle by repeatAction() ie. while(keepRunning){ ... } and is set to false by stoprepeat.setOnClickListener?
So I'm making my first Android app and I'm trying to get it to allow the user to pick a video from their gallery before seeing the video and the video's current details in the next activity.
My problem is that when I use FFmpegMediaMetadataRetriever and pass it the video's filepath, I receive the error java.lang.IllegalArgumentException: setDataSource failed: status = 0xFFFFFFFF.
I've heard through the grapevine that this means my filepath is invalid. When I Log.d the filepath, I get content://media/external/file/3565, which to me looks like a proper filepath!
I hope somebody can help me figure this out.
Here is my activity class for context:
class NewProject : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_project)
val videoPath = intent.getStringExtra("video")
initVideo(videoPath)
backButtonText.setOnClickListener{ goBack() }
}
private fun goBack() {
val intent = Intent(this,MainActivity::class.java)
startActivity(intent)
}
private fun initVideo(videoPath:String) {
newProjVideoView.setVideoPath(videoPath)
newProjVideoView.start()
newProjVideoView.setOnCompletionListener {
newProjVideoView.pause()
}
getVideoMetadata(videoPath)
}
private fun getVideoMetadata(videoPath: String) {
try {
e("videoPath", videoPath)
val receiver = FFmpegMediaMetadataRetriever()
receiver.setDataSource(videoPath)
} catch (e:IOException) {
e("retrieve1","There was an issue", e)
}
}
}
I'm also happy to hear any constructive feedback on my code!
Please, thank you and have a nice day!
So, I think my issue stemmed from trying to pass the video through an intent and then running the MetadataRetriever. I solved it by getting all the info in the previous activity before passing each value as an extra to be used on the next screen.
TL;DR: I have successfully created and coupled (via a subscription) an activity to a media browser service. This media browser service can continue running and play music in the background. I'd like to be able to refresh the content at some stage, either when the app comes to the foreground again or during a SwipeRefreshLayout event.
I have the following functionality I'd like to implement:
Start a MediaBrowserServiceCompat service.
From an activity, connect to and subscribe to the media browser service.
Allow the service to continue running and playing music while the app is closed.
At a later stage, or on a SwipeRefreshLayout event, reconnect and subscribe to the service to get fresh content.
The issue I am receiving is that within a MediaBrowserService (after a subscription has been created) you can only call sendResult() once from the onLoadChildren() method, so the next time you try to subscribe to the media browser service using the same root, you get the following exception when sendResult() is called for the second time:
E/UncaughtException: java.lang.IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: MEDIA_ID_ROOT
at android.support.v4.media.MediaBrowserServiceCompat$Result.sendResult(MediaBrowserServiceCompat.java:602)
at com.roostermornings.android.service.MediaService.loadChildrenImpl(MediaService.kt:422)
at com.roostermornings.android.service.MediaService.access$loadChildrenImpl(MediaService.kt:50)
at com.roostermornings.android.service.MediaService$onLoadChildren$1$onSyncFinished$playerEventListener$1.onPlayerStateChanged(MediaService.kt:376)
at com.google.android.exoplayer2.ExoPlayerImpl.handleEvent(ExoPlayerImpl.java:422)
at com.google.android.exoplayer2.ExoPlayerImpl$1.handleMessage(ExoPlayerImpl.java:103)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5665)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)
I call the following methods to connect to and disconnect from the media browser (again, everything runs smoothly on first connection, but on the second connection I'm not sure how to refresh the content via a subscription):
override fun onStart() {
super.onStart()
mMediaBrowser = MediaBrowserCompat(this, ComponentName(this, MediaService::class.java), connectionCallback, null)
if (!mMediaBrowser.isConnected)
mMediaBrowser.connect()
}
override fun onPause() {
super.onPause()
//Unsubscribe and unregister MediaControllerCompat callbacks
MediaControllerCompat.getMediaController(this#DiscoverFragmentActivity)?.unregisterCallback(mediaControllerCallback)
if (mMediaBrowser.isConnected) {
mMediaBrowser.unsubscribe(mMediaBrowser.root, subscriptionCallback)
mMediaBrowser.disconnect()
}
}
I unsubscribe and disconnect in onPause() instead of onDestroy() so that the subscription is recreated even if the activity is kept on the back-stack.
Actual method used for swipe refresh, in activity and service respectively:
Activity
if (mMediaBrowser.isConnected)
mMediaController?.sendCommand(MediaService.Companion.CustomCommand.REFRESH.toString(), null, null)
Service
inner class MediaPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {
...
override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
when(command) {
// Refresh media browser content and send result to subscribers
CustomCommand.REFRESH.toString() -> {
notifyChildrenChanged(MEDIA_ID_ROOT)
}
}
}}
Other research:
I have referred to the Google Samples code on Github, as well as...
https://github.com/googlesamples/android-MediaBrowserService
https://github.com/moondroid/UniversalMusicPlayer
Neither of the above repos seem to handle the issue of refreshing content after the media browser service has been created and the activity has subscribed at least once - I'd like to avoid restarting the service so that the music can continue playing in the background.
Possible related issues:
MediaBrowser.subscribe doesn't work after I get back to activity 1 from activity 2 (6.0.1 Android) --no effect on current issue
Calling you music service implementations notifyChildrenChanged(String parentId) will trigger the onLoadChildren and inside there, you can send a different result with result.sendResult().
What I did was that I added a BroadcastReceiver to my music service and inside it, I just called the notifyChildrenChanged(String parentId). And inside my Activity, I sent a broadcast when I changed the music list.
Optional (not Recommended) Quick fix
MusicService ->
companion object {
var musicServiceInstance:MusicService?=null
}
override fun onCreate() {
super.onCreate()
musicServiceInstance=this
}
//api call
fun fetchSongs(params:Int){
serviceScope.launch {
firebaseMusicSource.fetchMediaData(params)
//Edit Data or Change Data
notifyChildrenChanged(MEDIA_ROOT_ID)
}
}
ViewModel ->
fun fetchSongs(){
MusicService.musicServiceInstance?.let{
it.fetchSongs(params)
}
}
Optional (Recommended)
MusicPlaybackPreparer
class MusicPlaybackPreparer (
private val firebaseMusicSource: FirebaseMusicSource,
private val serviceScope: CoroutineScope,
private val exoPlayer: SimpleExoPlayer,
private val playerPrepared: (MediaMetadataCompat?) -> Unit
) : MediaSessionConnector.PlaybackPreparer {
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?
): Boolean {
when(command){
//edit data or fetch more data from api
"Add Songs"->{
serviceScope.launch {
firebaseMusicSource.fetchMediaData()
}
}
}
return false
}
override fun getSupportedPrepareActions(): Long {
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
}
override fun onPrepare(playWhenReady: Boolean) = Unit
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
firebaseMusicSource.whenReady {
val itemToPlay = firebaseMusicSource.songs.find { mediaId == it.description.mediaId }
playerPrepared(itemToPlay)
}
}
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) = Unit
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit
}
MusicServiceConnection
fun sendCommand(command: String, parameters: Bundle?) =
sendCommand(command, parameters) { _, _ -> }
private fun sendCommand(
command: String,
parameters: Bundle?,
resultCallback: ((Int, Bundle?) -> Unit)
) = if (mediaBrowser.isConnected) {
mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
resultCallback(resultCode, resultData)
}
})
true
} else {
false
}
ViewModel
fun fetchSongs(){
val args = Bundle()
args.putInt("nRecNo", 2)
musicServiceConnection.sendCommand("Add Songs", args )
}
MusicService ->
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
when(parentId) {
MEDIA_ROOT_ID -> {
val resultsSent = firebaseMusicSource.whenReady { isInitialized ->
if(isInitialized) {
try {
result.sendResult(firebaseMusicSource.asMediaItems())
if(!isPlayerInitialized && firebaseMusicSource.songs.isNotEmpty()) {
preparePlayer(firebaseMusicSource.songs, firebaseMusicSource.songs[0], true)
isPlayerInitialized = true
}
}
catch (exception: Exception){
// not recommend to notify here , instead notify when you
// change existing list in MusicPlaybackPreparer onCommand()
notifyChildrenChanged(MEDIA_ROOT_ID)
}
} else {
result.sendResult(null)
}
}
if(!resultsSent) {
result.detach()
}
}
}
}
My issue was unrelated to the MediaBrowserServiceCompat class. The issue was coming about because I was calling result.detach() in order to implement some asynchronous data fetching, and the listener I was using had both the parentId and result variables from the onLoadChildren method passed in and assigned final val rather than var.
I still don't fully understand why this occurs, whether it's an underlying result of using a Player.EventListener within another asynchronous network call listener, but the solution was to create and assign a variable (and perhaps someone else can explain this phenomenon):
// Create variable
var currentResult: Result<List<MediaBrowserCompat.MediaItem>>? = null
override fun onLoadChildren(parentId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>) {
// Use result.detach to allow calling result.sendResult from another thread
result.detach()
// Assign returned result to temporary variable
currentResult = result
currentParentId = parentId
// Create listener for network call
ChannelManager.onFlagChannelManagerDataListener = object : ChannelManager.Companion.OnFlagChannelManagerDataListener {
override fun onSyncFinished() {
// Create a listener to determine when player is prepared
val playerEventListener = object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when(playbackState) {
Player.STATE_READY -> {
if(mPlayerPreparing) {
// Prepare content to send to subscribed content
loadChildrenImpl(currentParentId, currentResult as MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>)
mPlayerPreparing = false
}
}
...
}
}
}
}