I'm creating an audiobook player, and I'm using MediaSessionCompat related classes to handle notifications. My code is heavily inspired by the android-MediaBrowserService samples ( https://github.com/googlearchive/android-MediaBrowserService ), and i'm not quite understanding it all for the moment ( the createContentIntent espicially )
Here is my simple class in charge of building notifications from a bookPlayer providing metadata and playbackstate data
class PlayerNotificationManager(val playerService: PlayerService) {
val CHANNEL_ID = "pylvain.gamma"
val NOTIFICATION_ID = 412
private val REQUEST_CODE = 501
val notificationManager =
playerService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val playAction: NotificationCompat.Action =
NotificationCompat.Action (
android.R.drawable.ic_media_pause,
"PAUSE",
buildMediaButtonPendingIntent(
playerService,
PlaybackStateCompat.ACTION_PAUSE
)
)
fun getNotification(bookPlayer: BookPlayer): Notification {
if (isAndroidOOrHigher()) createChannel()
val style = androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(playerService.sessionToken)
.setShowCancelButton(true)
.setShowActionsInCompactView(0)
.setCancelButtonIntent(
buildMediaButtonPendingIntent(
playerService,
PlaybackStateCompat.ACTION_STOP
)
)
val builder = NotificationCompat.Builder(playerService, CHANNEL_ID)
.addAction(playAction)
.setStyle(style)
.setSmallIcon(R.drawable.ic_music_rest_quarter)
.setContentIntent(createContentIntent())
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
return builder.build()
}
private fun createContentIntent(): PendingIntent { //TODO: Understand that
Timber.i("Creating Intent")
val openUI = Intent(playerService, MainActivity::class.java)
openUI.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
return PendingIntent.getActivity(
playerService, 0, openUI, PendingIntent.FLAG_UPDATE_CURRENT
)
}
The notification is showing perfectly fine with metadata
Here is my MediaBrowserService handling the media session, where I registered the callbacks. The bookplayer is constructed and injected with Koin. :
class PlayerService : MediaBrowserServiceCompat() {
private lateinit var playerNotificationManager: PlayerNotificationManager
lateinit var session: MediaSessionCompat
private val bookPlayer: BookPlayer by inject()
override fun onCreate() {
super.onCreate()
session = MediaSessionCompat(this, "MusicService")
session.apply {
setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
setPlaybackState(bookPlayer.playbackState)
setMetadata(bookPlayer.getMetadata())
setCallback(callbacks)
setActive(true)
}
setSessionToken(session.sessionToken)
playerNotificationManager = PlayerNotificationManager(this)
val notification = playerNotificationManager.getNotification(bookPlayer)
startForeground(playerNotificationManager.NOTIFICATION_ID, notification)
}
override fun onTaskRemoved(rootIntent: Intent?) { //TODO
super.onTaskRemoved(rootIntent)
stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
return BrowserRoot("root", null)
}
override fun onLoadChildren(
parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
result.sendResult(null);
}
override fun onDestroy() {
session.release()
}
val callbacks = object : MediaSessionCompat.Callback() {
override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
Timber.i("Test")
super.onCommand(command, extras, cb)
}
override fun onPrepare() {
Timber.i("Preparing")
}
override fun onPlay() {
Timber.i("Playing")
bookPlayer.pause()
}
override fun onPause() {
Timber.i("Pausing")
bookPlayer.pause()
}
override fun onSkipToNext() {}
override fun onSkipToPrevious() {}
override fun onStop() {}
override fun onSeekTo(pos: Long) {}
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean = true
}
}
Then the service is started from the main activity with
startService(Intent(mainContext, PlayerService::class.java))
I also added this to my Android manifest
<service android:name=".playerservice.PlayerService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
None of the callbacks are called whenever I push the button. when i do, the app log the following text :
D/MediaBrowserCompat: Connecting to a MediaBrowserService.
and nothing happens ...
I've searched the entire internet and I'm completely clueless, but it's surely something simple. Can someone help me ? Thank you very much in advance <3
The callback worked ... Just not the way intended. It turns out that the play action button was calling
override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean = true
I deleted the function, and ... It works ...
Thank you for your attention !
If you want to get media button, you have to play something.
Try to play dummy audio, when your service is started
// play dummy audio
AudioTrack at = new AudioTrack(AudioManager.STREAM_MUSIC, 48000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT,
AudioTrack.getMinBufferSize(48000, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT), AudioTrack.MODE_STREAM);
at.play();
// a little sleep
at.stop();
at.release();
https://stackoverflow.com/a/50678833/9891730 - this is my answer before
Related
I build a web radio player with Media3 1.0.0-beta03. I use the sample code from
Developers page.
It's generated a media notification automatically but I don't know how to add Title and sub title to this.
Here is my media service:
class PlaybackService : MediaSessionService(), MediaSession.Callback {
private object LC {
lateinit var exoPlayer: ExoPlayer
lateinit var mediaSession: MediaSession
}
override fun onCreate() {
super.onCreate()
log("----------------------------- MediaSessionService, onCreate")
LC.exoPlayer = ExoPlayer.Builder(this).build()
LC.exoPlayer.addListener(ExoListener())
LC.exoPlayer.setAudioAttributes(AudioAttributes.Builder().setContentType(AUDIO_CONTENT_TYPE_MUSIC).setUsage(USAGE_MEDIA).build(),true)
LC.mediaSession = MediaSession.Builder(this, LC.exoPlayer).setCallback(this).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = LC.mediaSession
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)
}
override fun onDestroy() {
log("----------------------------- MediaSessionService, onDestroy")
LC.exoPlayer.stop()
LC.exoPlayer.release()
LC.mediaSession.release()
super.onDestroy()
exitProcess(0)
}
}
I tryed the onUpdateNotification
Yesss, thank you TG. Kahsay
Notification manager is not needed.
class PlaybackService : MediaSessionService(), MediaSession.Callback {
private object LC {
lateinit var exoPlayer: ExoPlayer
lateinit var mediaSession: MediaSession
}
#SuppressLint("UnsafeOptInUsageError")
override fun onCreate() {
super.onCreate()
log("----------------------------- MediaSessionService, onCreate")
LC.exoPlayer = ExoPlayer.Builder(this).build()
LC.exoPlayer.addListener(BackgroundService())
LC.exoPlayer.setAudioAttributes(AudioAttributes.Builder().setContentType(AUDIO_CONTENT_TYPE_MUSIC).setUsage(USAGE_MEDIA).build(),true)
LC.mediaSession = MediaSession.Builder(this, LC.exoPlayer).setCallback(this).build()
setMediaNotificationProvider(object : MediaNotification.Provider{
override fun createNotification(
mediaSession: MediaSession,
customLayout: ImmutableList<CommandButton>,
actionFactory: MediaNotification.ActionFactory,
onNotificationChangedCallback: MediaNotification.Provider.Callback
): MediaNotification {
// This run every time when I press buttons on notification bar:
return updateNotification(mediaSession)
}
override fun handleCustomCommand(session: MediaSession, action: String, extras: Bundle): Boolean { return false }
})
}
#SuppressLint("UnsafeOptInUsageError")
private fun updateNotification(session: MediaSession): MediaNotification {
val notify = NotificationCompat.Builder(this,"Radio")
.setSmallIcon(R.drawable.ic_launcher_foreground)
// This is globally changed every time when
// I add a new MediaItem from background service
.setContentTitle(GL.MEDIA.radio)
.setContentText(GL.MEDIA.artist)
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
.build()
return MediaNotification(1, notify)
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession = LC.mediaSession
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)
}
override fun onDestroy() {
log("----------------------------- MediaSessionService, onDestroy")
LC.exoPlayer.stop()
LC.exoPlayer.release()
LC.mediaSession.release()
super.onDestroy()
}
}
Update :
There is also another way, which I found out does the Job better.
In onCreate() function of MediaSessionService, We can set a MediaNotificationProvider like so.
private lateinit var nBuilder: NotificationCompat.Builder
override fun onCreate(){
super.onCreate()
// init notificationCompat.Builder before setting the MediaNotificationProvider
this.setMediaNotificationProvider(object : MediaNotification.Provider{
override fun createNotification(
mediaSession: MediaSession,// this is the session we pass to style
customLayout: ImmutableList<CommandButton>,
actionFactory: MediaNotification.ActionFactory,
onNotificationChangedCallback: MediaNotification.Provider.Callback
): MediaNotification {
createNotification(mediaSession)
// notification should be created before you return here
return MediaNotification(NOTIFICATION_ID,nBuilder.build())
}
override fun handleCustomCommand(
session: MediaSession,
action: String,
extras: Bundle
): Boolean {
TODO("Not yet implemented")
}
})
}
fun createNotification(session: MediaSession) {
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(NotificationChannel(notification_id,"Channel", NotificationManager.IMPORTANCE_LOW))
// NotificationCompat.Builder here.
nBuilder = NotificationCompat.Builder(this,notification_id)
// Text can be set here
// but I believe setting MediaMetaData to MediaSession would be enough.
// I havent tested it deeply yet but did display artist from session
.setSmallIcon(R.drawable.your_drawable)
.setContentTitle("your Content title")
.setContentText("your content text")
// set session here
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
// we don build.
}
and finally if you want to update notification info yourself
you can do so by calling a function like this..
private fun updateNotification(/*parameter*/){
nBuilder.setContentTitle("text")
nBuilder.setContentText("subtext")
nManager.notify(NOTIFICATION_ID,nBuilder.build())
}
Turns out, its very simple.
There is a method to override in MediaService class called onUpdateNotification(). it provides the media session for us.
so we can override it and create our own NotificationCompat
// Override this method in your service
override fun onUpdateNotification(session: MediaSession) {
createNotification(session) //calling method where we create notification
}
and in our createNotification() method we create the notification and set its style with a MediaStyleHelper.MediaStyle() set the session parameter there
like in the following.
and create the notification as always
fun createNotification(session: MediaSession) {
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(NotificationChannel(notification_id,"Channel", NotificationManager.IMPORTANCE_LOW))
// NotificationCompat here.
val notificationCompat = NotificationCompat.Builder(this,notification_id)
// Text can be set here
// but I believe setting MediaMetaData to MediaSession would be enough.
// I havent tested it deeply yet but did display artist from session
.setSmallIcon(R.drawable.your_drawable)
.setContentTitle("your Content title")
.setContentText("your content text")
// set session here
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
.build()
notificationManager.notify(1,notificationCompat)
}
I hope this helps and isn't too late.
Edit:
and another cleaner option is to just Create MediaItem wit desired MediaMetaData and add it to ExoPlayer. If source is Hls, try not adding title to MediaMetaData.
I am trying to implement media player, but it stops after 2 minutes of playtime - like it is not in foreground service. The foreground service starts from fragment and it should live only while the fragment is created.
Could someone help, please?
Service:
class MediaPlayerForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(1, notificationToDisplayServiceInform(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
lockCpu()
fileName = intent?.getStringExtra("fileName")
handler = Looper.myLooper()?.let { Handler(it) }
player = MediaPlayer().apply {
setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK)
}
player!!.setScreenOnWhilePlaying(true)
val afd: AssetFileDescriptor = applicationContext.assets.openFd(fileName!!)
player!!.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length);
afd.close()
player!!.setOnPreparedListener {
handler!!.postDelayed(runnableCheck!!, 200)
}
player!!.prepareAsync()
return START_REDELIVER_INTENT
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
channelId,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(
NotificationManager::class.java
)
manager.createNotificationChannel(serviceChannel)
}
}
private fun notificationToDisplayServiceInform(): Notification {
createNotificationChannel()
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, PendingIntent.FLAG_MUTABLE
)
return NotificationCompat.Builder(this, channelId)
.setContentTitle("Simple Foreground Service")
.setContentText("Explain about the service")
.setSmallIcon(R.drawable.player_play)
.setContentIntent(pendingIntent)
.build()
}
private fun lockCpu() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Movapp::WakeLockFairyTale").apply {
acquire()
}
}
}
}
Fragment code:
class PlayerFragment : Fragment() {
private fun startMediaPlayerService(fileName: String){
Intent(context, MediaPlayerForegroundService::class.java).also {
it.putExtra("fileName", fileName)
}.also {
context!!.startForegroundService(it)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
...
....
startMediaPlayerService("stories/${slug}/${langPair.to.langCode}.mp3")
return root
}
override fun onDestroyView() {
super.onDestroyView()
stopMediaPlayerService()
_binding = null
}
}
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="cz.movapp.app">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
<service android:name=".MediaPlayerForegroundService"
android:foregroundServiceType="mediaPlayback"
android:enabled="true"
android:exported="false"
/>
</application>
</manifest>
I tried a lot of googling, read several articles, and I have no clue why it stops right after two minutes. My android is version 13. In emulator, it works. If I set the player.isLooping = true, it plays the 2 minutes in a loop, which means the service lives.
EDITED:
I think I am a bit closer. Seems like the problem is in:
val afd: AssetFileDescriptor = applicationContext.assets.openFd(fileName!!)
player!!.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length);
afd.close()
The afd.length seems to be incorrect. afd.declaredLength is also too short (the same). This happens with more than one file.
I still do not know how to fix it.
EDITED2:
The problem with 2 minutes limit seems to be only for mp3 files with a bitrate lower than 96kbps. I can also say .ogg files work fine. Based on the documentation, the spec: Mono/Stereo 8-320Kbps constant (CBR) or variable bit-rate (VBR) is supported. Is it a bug?
It is possible that this is a bug. Have you experienced this problem on a Pixel device? I have seen some Pixel devices that had this behavior while it worked fine on other phones.
I am using the latest version of Exoplayer (2.16.1). I create my foreground service notification using the code below but it comes with a notification sound. How can I prevent this sound?
override fun onCreate() {
super.onCreate()
playerNotificationManager = PlayerNotificationManager
.Builder(this, 9998, NOTIFICATION_CHANNEL_ID)
.setMediaDescriptionAdapter(PlayerNotificationAdapter())
.setChannelImportance(IMPORTANCE_HIGH)
.setNotificationListener(object : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
ongoing: Boolean
) {
super.onNotificationPosted(notificationId, notification, ongoing)
if (ongoing) {
startForeground(notificationId, notification)
}
}
override fun onNotificationCancelled(
notificationId: Int,
dismissedByUser: Boolean
) {
super.onNotificationCancelled(notificationId, dismissedByUser)
stopSelf()
stopForeground(false)
}
})
.setChannelNameResourceId(R.string.NotificationChannelName)
.setChannelDescriptionResourceId(R.string.NotificationChannelDescription)
.build()
playerNotificationManager.setSmallIcon(R.drawable.ic_play)
playerNotificationManager.setPriority(PRIORITY_MAX)
playerNotificationManager.setPlayer(musicPlayerManager.exoPlayer)
mediaSessionCompat = MediaSessionCompat(this, MEDIA_SESSION_TAG)
mediaSessionCompat.isActive = true
playerNotificationManager.setMediaSessionToken(mediaSessionCompat.sessionToken)
mediaSessionConnector = MediaSessionConnector(mediaSessionCompat)
val timelineQueueNavigator = object : TimelineQueueNavigator(mediaSessionCompat) {
override fun getMediaDescription(
player: Player,
windowIndex: Int
): MediaDescriptionCompat {
return MediaDescriptionCompat.Builder().build()
}
}
mediaSessionConnector.setQueueNavigator(timelineQueueNavigator)
mediaSessionConnector.setPlayer(musicPlayerManager.exoPlayer)
}
Please note that I want to use PlayerNotificationManager of Exoplayer to create the notification.
you can set : playerNotificationManager.setPriority(PRIORITY_DEFAULT) to make silent notification in this case
I'm trying to run exoplayer in a foreground service (not a MediaBrowserServiceCompat).
Here is my service -
#AndroidEntryPoint
class PodcastPlayerService: Service() {
#Inject
lateinit var dataSourceFactory: DefaultDataSourceFactory
#Inject
lateinit var exoPlayer: SimpleExoPlayer
lateinit var podcastNotificationManager: PodcastNotificationManager
lateinit var podcast: Podcast
override fun onDestroy() {
super.onDestroy()
exoPlayer.release()
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
exoPlayer.stop()
exoPlayer.release()
}
override fun onBind(p0: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("Pit stop", "2")
val b = intent!!.getBundleExtra("test")
if (b != null) {
Log.i("Pit stop", "3")
podcast = b.getParcelable<Podcast>(ArgumentKeyAndValues.KEY_PODCAST)!!
}
val mediaSource = buildMediaSource(Uri.parse("https://something.etc/file.mp3"))
if (mediaSource != null) {
exoPlayer.prepare(mediaSource)
exoPlayer.playWhenReady = true
podcastNotificationManager =
PodcastNotificationManager(
this,
PodcastPlayerNotificationListener(this)
)
podcastNotificationManager.showNotification(exoPlayer)
}
return START_STICKY
}
private fun buildMediaSource(uri: Uri): MediaSource? {
return ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(uri)
}
}
Notification Listener -
class PodcastPlayerNotificationListener(private val podcastPlayerService: PodcastPlayerService):
PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
ongoing: Boolean) {
super.onNotificationPosted(notificationId, notification, ongoing)
podcastPlayerService.apply {
if(ongoing) {
ContextCompat.startForegroundService(this,
Intent(applicationContext, this::class.java))
startForeground(OtherConstants.PODCAST_NOTIFICATION_ID, notification)
}
}
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
super.onNotificationCancelled(notificationId, dismissedByUser)
podcastPlayerService.apply {
stopForeground(true)
stopSelf()
}
}
}
Podcast Notification Manager -
class PodcastNotificationManager(private val context: Context,
notificationListener: PlayerNotificationManager.NotificationListener) {
private val notificationManager: PlayerNotificationManager
init {
notificationManager = PlayerNotificationManager.createWithNotificationChannel(
context,
OtherConstants.NOTIFICATION_CHANNEL_ID,
R.string.notification_channel_name,
R.string.notification_channel_description,
OtherConstants.PODCAST_NOTIFICATION_ID,
DescriptionAdapter(),
notificationListener
).apply {
setSmallIcon(R.drawable.exo_icon_play)
}
}
fun showNotification(player: Player) {
notificationManager.setPlayer(player)
}
private inner class DescriptionAdapter : PlayerNotificationManager.MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): String {
val window = player.currentWindowIndex
return "Title"
}
override fun getCurrentContentText(player: Player): String? {
val window = player.currentWindowIndex
return "Description"
}
override fun getCurrentLargeIcon(
player: Player,
callback: BitmapCallback
): Bitmap? = null
override fun createCurrentContentIntent(player: Player): PendingIntent? {
val window = player.currentWindowIndex
return null
}
}
}
Here is how I start the service -
val intent = Intent(context, PodcastPlayerService::class.java)
val serviceBundle = Bundle()
serviceBundle.putParcelable("test", podcast)
intent.putExtra(ArgumentKeyAndValues.KEY_PODCAST, serviceBundle)
context?.let { Util.startForegroundService(it, intent) }
However, when I do this onStartCommand keeps getting called (I'm assuming that the OS keeps killing my service for some reason and START_STICKY forces it to start again) and nothing happens.
If I place the media, notification manager and listener code in onCreate the service works fine.
Where am I going wrong?
Turns out I was starting the foreground service twice -
ContextCompat.startForegroundService(this,
Intent(applicationContext, this::class.java))
startForeground(OtherConstants.PODCAST_NOTIFICATION_ID, notification)
I have an app that uses Service class to perform task in foreground.
This service also contains a Handler object to run same function multiple times. I want to change attributes in my activity_main.xml while functions are running in Service. For example when function calculates something in Service the result prints in TextView.
How it would be correct access activity_main's objects to retrieve and change their values and attributes?
Here is what I have:
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private var notificationManager: NotificationManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonStart.setOnClickListener{
buttonStart.isEnabled = false
buttonStop.isEnabled = true
IdListener.startService(this, "Foreground Service is running...")
}
}
}
IdListener.kt:
class IdListener : Service() {
private val CHANNEL_ID = "ForegroundService Kotlin"
private lateinit var mainHandler: Handler
private lateinit var mRunnable: Runnable
companion object {
fun startService(context: Context, message: String) {
val startIntent = Intent(context, IdListener::class.java)
startIntent.putExtra("inputExtra", message)
ContextCompat.startForegroundService(context, startIntent)
}
fun stopService(context: Context) {
val stopIntent = Intent(context, IdListener::class.java)
context.stopService(stopIntent)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
mainHandler = Handler()
mRunnable = Runnable { showRandomNumber(tm) }
mainHandler.postDelayed(mRunnable, 1000)
val input = intent?.getStringExtra("inputExtra")
createNotificationChannel()
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service Kotlin Example")
.setContentText(input)
.setSmallIcon(R.drawable.ic_notofication)
.setContentIntent(pendingIntent)
.build()
startForeground(1, notification)
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
mainHandler.removeCallbacks(mRunnable)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(CHANNEL_ID, "Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT)
val manager = getSystemService(NotificationManager::class.java)
manager!!.createNotificationChannel(serviceChannel)
}
}
/// function in which I want elements from activity_main.xml to be changed
fun showRandomNumber(manager: TelephonyManager){
myTextView.text = "Working..."
mainHandler.postDelayed(mRunnable, 1000)
}
}
Here's how I'd probably handle your case. I don't know exactly what you're doing, but I'm just having the text view show "Working..." when it starts the service until there's an ID available. I haven't tested this and haven't worked with services in a long time, so you might want other input.
object IdServiceData {
val id = MutableLiveData<String>()
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...
myTextView.text = "Working..." // should really use string resource here.
IdServiceData.id.observe(this) {
myTextView.text = it.value
}
}
}
When an Activity or Fragment observes a LiveData, they automatically stop observing when they are destroyed, so they are not leaked. So your Activity can be destroyed and recreated multiple times while the Service is running and it will keep getting the proper updates.
class IdListener : Service() {
//...
private fun broadcastNewId(id: String){
mainHandler.post {
IdServiceData.id.value = id
}
}
}
If you want better encapsulation, I suppose you could abstract out the MutableLiveData by creating a separate IdServiceDataProvider that has the MutableLiveData and is used by the service, and the IdServiceData would reference the data like this: val id: LiveData<String> = IdServiceDataProvider.id