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
Related
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)
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 need to intercept the events of a outgoing call made by the device framework.
Following the android guide, i'm stopped at point 3 The telecom subsystem binds to your app's ConnectionService implementation., that is i have come to this point:
Call flow
val telecomManager :TelecomManager= getSystemService(
TELECOM_SERVICE
) as TelecomManager
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE),
2333)
} else {
try {
val phoneAccountHandle = PhoneAccountHandle(ComponentName(
applicationContext,
MyConnectionService::class.java
), "ID999")
telecomManager.registerPhoneAccount(PhoneAccount.builder(
phoneAccountHandle,
"label"
).setCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER) .build())
val extras = Bundle()
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
telecomManager.placeCall(Uri.parse("tel:$phoneNumber"), extras)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
ConnectionService
class MyConnectionService : ConnectionService() {
private val TAG = "mycnnser"
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate: ")
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: ")
return super.onStartCommand(intent, flags, startId)
}
override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle, request: ConnectionRequest): Connection {
Log.d(TAG, "onCreateIncomingConnection: ")
return super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
}
override fun onCreateIncomingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle, request: ConnectionRequest) {
Log.d(TAG, "onCreateIncomingConnectionFailed: ")
super.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request)
}
override fun onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount: PhoneAccountHandle, request: ConnectionRequest) {
Log.d(TAG, "onCreateOutgoingConnectionFailed: ")
super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request)
}
override fun onCreateOutgoingConnection(connectionManagerPhoneAccount: PhoneAccountHandle, request: ConnectionRequest): Connection {
Log.d(TAG, "onCreateOutgoingConnection: ")
return super.onCreateOutgoingConnection(connectionManagerPhoneAccount, request)
}
override fun onCreateOutgoingHandoverConnection(fromPhoneAccountHandle: PhoneAccountHandle, request: ConnectionRequest): Connection {
Log.d(TAG, "onCreateOutgoingHandoverConnection: ")
return super.onCreateOutgoingHandoverConnection(fromPhoneAccountHandle, request)
}
override fun onCreateIncomingHandoverConnection(fromPhoneAccountHandle: PhoneAccountHandle, request: ConnectionRequest): Connection {
Log.d(TAG, "onCreateIncomingHandoverConnection: ")
return super.onCreateIncomingHandoverConnection(fromPhoneAccountHandle, request)
}
override fun onHandoverFailed(request: ConnectionRequest, error: Int) {
super.onHandoverFailed(request, error)
Log.d(TAG, "onHandoverFailed: ")
}
override fun onConference(connection1: Connection, connection2: Connection) {
super.onConference(connection1, connection2)
Log.d(TAG, "onConference: ")
}
override fun onRemoteConferenceAdded(conference: RemoteConference) {
super.onRemoteConferenceAdded(conference)
Log.d(TAG, "onRemoteConferenceAdded: ")
}
override fun onRemoteExistingConnectionAdded(connection: RemoteConnection) {
super.onRemoteExistingConnectionAdded(connection)
Log.d(TAG, "onRemoteExistingConnectionAdded: ")
}
override fun onConnectionServiceFocusLost() {
super.onConnectionServiceFocusLost()
Log.d(TAG, "onConnectionServiceFocusLost: ")
}
override fun onConnectionServiceFocusGained() {
super.onConnectionServiceFocusGained()
Log.d(TAG, "onConnectionServiceFocusGained: ")
}}
Manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thorny.myapplication">
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29"/>
<uses-permission android:name="android.permissions.READ_PHONE_NUMBERS"/>
<uses-permission
android:name="android.permission.CALL_PHONE" />
<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.MyApplication">
<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=".MyConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
</application>
</manifest>
Problem: the call starts through the device framework but all the service logs are never triggered.
My Android Version is 10.
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.