How to use service in jetpack compose - android

I want to use service in jetpack compose. I am trying to bind services in compose app but it always return null to me. I tried this stack overflow. But it didn't work to me. Anyone guide me on this ?
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.IBinder
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
#Composable
inline fun <reified BoundService : Service, reified BoundServiceBinder : Binder> rememberBoundLocalService(
crossinline getService: #DisallowComposableCalls BoundServiceBinder.() -> BoundService,
): BoundService? {
val context: Context = LocalContext.current
var boundService: BoundService? by remember(context) { mutableStateOf(null) }
val serviceConnection: ServiceConnection = remember(context) {
object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
boundService = (service as BoundServiceBinder).getService()
}
override fun onServiceDisconnected(arg0: ComponentName) {
boundService = null
}
}
}
DisposableEffect(context, serviceConnection) {
context.bindService(Intent(context, BoundService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
onDispose { context.unbindService(serviceConnection) }
}
return boundService
}
BluetoothService.kt
class BluetoothService : Service() {
companion object {
const val ACTION_GATT_CONNECTED =
"com.abc.app.ACTION_GATT_CONNECTED"
const val ACTION_GATT_DISCONNECTED =
"com.abc.app.ACTION_GATT_DISCONNECTED"
private const val STATE_DISCONNECTED = 0
private const val STATE_CONNECTED = 2
}
private var connectionState = STATE_DISCONNECTED
private var bluetoothAdapter: BluetoothAdapter? = null
private val binder = LocalBinder()
private var bluetoothGatt: BluetoothGatt? = null
private val bluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// successfully connected to the GATT Server
connectionState = STATE_CONNECTED
broadcastUpdate(ACTION_GATT_CONNECTED)
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// disconnected from the GATT Server
connectionState = STATE_DISCONNECTED
broadcastUpdate(ACTION_GATT_DISCONNECTED)
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
inner class LocalBinder : Binder() {
fun getService(): BluetoothService {
return this#BluetoothService
}
}
fun initialize(adapter: BluetoothAdapter?): Boolean {
bluetoothAdapter = adapter
if (bluetoothAdapter == null) {
logE(">> Unable to obtain a BluetoothAdapter.")
return false
}
return true
}
fun connect(address: String): Boolean {
bluetoothAdapter?.let { adapter ->
try {
val device = adapter.getRemoteDevice(address)
// connect to the GATT server on the device
bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
return true
} catch (exception: IllegalArgumentException) {
logE("Device not found with provided address.")
return false
}
} ?: run {
logE("BluetoothAdapter not initialized")
return false
}
}
override fun onUnbind(intent: Intent?): Boolean {
close()
return super.onUnbind(intent)
}
private fun close() {
bluetoothGatt?.let { gatt ->
gatt.close()
bluetoothGatt = null
}
}
private fun broadcastUpdate(action: String) {
val intent = Intent(action)
sendBroadcast(intent)
}
}
I am trying to use bluetoothService it always return null when I check in the condition
BluetoothConnectionContentStateful.kt
#OptIn(ExperimentalLifecycleComposeApi::class)
#Composable
fun BluetoothConnectionContentStateful(
context: Context = LocalContext.current,
viewModel: BloodPressurePairViewModel = getViewModel(),
router: Router = get()
) {
val activity = context as ComponentActivity
val bluetoothService = rememberBoundLocalService<BluetoothService, BluetoothService.LocalBinder> { getService() }
val scanDeviceList = viewModel.scanLeDevices.collectAsStateWithLifecycle()
if (bluetoothService != null) {
if (!bluetoothService.initialize(rememberPairScreenState.bluetoothAdapter)) {
activity.logE(">> Unable to initialize Bluetooth")
} else {
activity.logE(">> initialize Bluetooth")
}
}else{
activity.logE(">> bluetoothService null")
}
}
bluetoothService always return null. Any idea about that?

Related

how do i get the set notification in BLE nordic

i am trying to set notification callback in BLE nordic, where i am using the Android BLE library (Nordic Github). But i not ablet to get the notification event when i am changing the value of the characteristics.
`
class BleManagerHP1T(context: Context) : BleManager(context) {
override fun getGattCallback(): BleManagerGattCallback = GattCallback()
override fun log(priority: Int, message: String) {
if (BuildConfig.DEBUG || priority == Log.ERROR) {
Log.println(priority, GattService.TAG, message)
}
}
private inner class GattCallback : BleManagerGattCallback() {
private var myCharacteristic: BluetoothGattCharacteristic? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null
#SuppressLint("MissingPermission")
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(GattService.MyServiceProfile.MY_SERVICE_UUID)
myCharacteristic =
service?.getCharacteristic(GattService.MyServiceProfile.MY_CHARACTERISTIC_UUID)
val myCharacteristicProperties = myCharacteristic?.properties ?: 0
Log.d(TAG, "isRequiredServiceSupported: notify ${(myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)}")
rxCharacteristic = service?.getCharacteristic(GattService.MyServiceProfile.RX_CHARACTERISTIC_UUID)
val obj = JSONObject()
obj.put("OPCODE","PROVISION")
rxCharacteristic?.value = obj.toString().encodeToByteArray()
val rxRead = gatt.writeCharacteristic(rxCharacteristic)
Log.d(TAG, "isRequiredServiceSupported: Read $rxRead")
return (myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
}
override fun initialize() {
enableNotifications(myCharacteristic).enqueue()
requestMtu(260).enqueue();
setNotificationCallback(myCharacteristic).with { _, data ->
Log.d(TAG, "initialize: TX char Notification Called")
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
Log.d(TAG, "initialize: TX char value $value")
}
}
requestMtu(260).enqueue();
enableNotifications(rxCharacteristic).enqueue()
setNotificationCallback(rxCharacteristic).with { _, data ->
Log.d(TAG, "initialize: RX char Notification Called")
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
Log.d(TAG, "initialize: RX char value $value")
}
}
beginAtomicRequestQueue()
.add(enableNotifications(myCharacteristic)
.fail { _: BluetoothDevice?, status: Int ->
log(Log.ERROR, "Could not subscribe: $status")
disconnect().enqueue()
}
)
.done {
log(Log.INFO, "Target initialized")
}
.enqueue()
}
override fun onServicesInvalidated() {
myCharacteristic = null
}
}
override fun readCharacteristic(characteristic: BluetoothGattCharacteristic?): ReadRequest {
return Request.newReadRequest(characteristic)
}
}
`
gatt connection is establishing perfectly fine, using this code.
`
val bleManager = BleManagerHP1T(this#ControllerActivity)
synchronized (this) {
bleManager.connect(deviceMainList[position]).useAutoConnect(false).enqueue()
}
`
here is the gatt service file .
`
class GattService : Service() {
private val defaultScope = CoroutineScope(Dispatchers.Default)
private lateinit var bluetoothObserver: BroadcastReceiver
private var myCharacteristicChangedChannel: SendChannel<String>? = null
private val clientManagers = mutableMapOf<String, ClientManager>()
// val connect = BleManager()
#RequiresApi(Build.VERSION_CODES.O)
override fun onCreate() {
super.onCreate()
// Setup as a foreground service
val notificationChannel = NotificationChannel(
GattService::class.java.simpleName,
resources.getString(R.string.gatt_service_name),
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationService =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationService.createNotificationChannel(notificationChannel)
val notification = NotificationCompat.Builder(this, GattService::class.java.simpleName)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(resources.getString(R.string.gatt_service_name))
.setContentText(resources.getString(R.string.gatt_service_running_notification))
.setAutoCancel(true)
startForeground(1, notification.build())
// Observe OS state changes in BLE
bluetoothObserver = object : BroadcastReceiver() {
#SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val bluetoothState = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
-1
)
when (bluetoothState) {
BluetoothAdapter.STATE_ON -> enableBleServices()
BluetoothAdapter.STATE_OFF -> disableBleServices()
}
}
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
val device =
intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
Log.d(TAG, "Bond state changed for device ${device?.address}: ${device?.bondState}")
when (device?.bondState) {
BluetoothDevice.BOND_BONDED -> addDevice(device)
BluetoothDevice.BOND_NONE -> removeDevice(device)
}
}
}
}
}
registerReceiver(bluetoothObserver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
registerReceiver(bluetoothObserver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
// Startup BLE if we have it
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (bluetoothManager.adapter?.isEnabled == true) enableBleServices()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(bluetoothObserver)
disableBleServices()
}
override fun onBind(intent: Intent?): IBinder? =
when (intent?.action) {
DATA_PLANE_ACTION -> {
DataPlane()
}
else -> null
}
override fun onUnbind(intent: Intent?): Boolean =
when (intent?.action) {
DATA_PLANE_ACTION -> {
myCharacteristicChangedChannel = null
true
}
else -> false
}
/**
* A binding to be used to interact with data of the service
*/
inner class DataPlane : Binder() {
fun setMyCharacteristicChangedChannel(sendChannel: SendChannel<String>) {
myCharacteristicChangedChannel = sendChannel
}
}
#SuppressLint("MissingPermission")
private fun enableBleServices() {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (bluetoothManager.adapter?.isEnabled == true) {
Log.i(TAG, "Enabling BLE services")
bluetoothManager.adapter.bondedDevices.forEach { device -> addDevice(device) }
} else {
Log.w(TAG, "Cannot enable BLE services as either there is no Bluetooth adapter or it is disabled")
}
}
private fun disableBleServices() {
clientManagers.values.forEach { clientManager ->
clientManager.close()
}
clientManagers.clear()
}
private fun addDevice(device: BluetoothDevice) {
if (!clientManagers.containsKey(device.address)) {
val clientManager = ClientManager()
clientManager.connect(device).useAutoConnect(true).enqueue()
clientManagers[device.address] = clientManager
}
}
private fun removeDevice(device: BluetoothDevice) {
clientManagers.remove(device.address)?.close()
}
/*
* Manages the entire GATT service, declaring the services and characteristics on offer
*/
companion object {
/**
* A binding action to return a binding that can be used in relation to the service's data
*/
const val DATA_PLANE_ACTION = "data-plane"
const val TAG = "gatt-service"
}
private inner class ClientManager : BleManager(this#GattService) {
override fun getGattCallback(): BleManagerGattCallback = GattCallback()
override fun log(priority: Int, message: String) {
if (BuildConfig.DEBUG || priority == Log.ERROR) {
Log.println(priority, TAG, message)
Log.d(TAG, "log: $message")
}
}
private inner class GattCallback : BleManagerGattCallback() {
private var myCharacteristic: BluetoothGattCharacteristic? = null
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(MyServiceProfile.MY_SERVICE_UUID)
myCharacteristic =
service?.getCharacteristic(MyServiceProfile.MY_CHARACTERISTIC_UUID)
val myCharacteristicProperties = myCharacteristic?.properties ?: 0
return (myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_READ != 0) &&
(myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
}
override fun initialize() {
setNotificationCallback(myCharacteristic).with { _, data ->
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
defaultScope.launch {
myCharacteristicChangedChannel?.send(value)
}
}
}
beginAtomicRequestQueue()
.add(enableNotifications(myCharacteristic)
.fail { _: BluetoothDevice?, status: Int ->
log(Log.ERROR, "Could not subscribe: $status")
disconnect().enqueue()
}
)
.done {
log(Log.INFO, "Target initialized")
}
.enqueue()
}
override fun onServicesInvalidated() {
myCharacteristic = null
}
}
}
object MyServiceProfile {
val MY_SERVICE_UUID: UUID = UUID.fromString("8d67d51a-801b-43cb-aea2-bbec9d1211fd")
val MY_CHARACTERISTIC_UUID: UUID = UUID.fromString("8d67d51c-801b-43cb-aea2-bbec9d1211fd")
val RX_CHARACTERISTIC_UUID: UUID = UUID.fromString("8d67d51b-801b-43cb-aea2-bbec9d1211fd")
}
}
`

forground service destroy on minimize app

I am developing an audio player app that works well but I am facing one problem if I minimize the app it kill my foreground service. I don't know why it's happening can anyone suggest me any solution
MainActivity
import android.Manifest
import android.app.PendingIntent
import android.content.*
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.text.Html
import android.util.Log
import android.view.WindowManager
import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.brahmakumaris.fragment.*
import com.brahmakumaris.model.getDashboard.RecentSong
import com.brahmakumaris.service.MusicPlayerService
import com.brahmakumaris.service.MyService
import com.brahmakumaris.util.*
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
import kotlin.collections.ArrayList
class MainActivity : AppCompatActivity(), OnFragmentInteractionListener {
override fun onFragmentInteraction(screen: String, model: RecentSong, songList: ArrayList<RecentSong>, position: Int) {
MyLog.e(TAG, screen)
when (screen) {
getString(R.string.speakers) -> {
bottom_navigation.selectedItemId = R.id.nav_speaker
supportFragmentManager.switch(
newFrag = SpeakerPageFragment.newInstance(),
tag = getString(R.string.speakers)
)
}
getString(R.string.classes) -> {
bottom_navigation.selectedItemId = R.id.nav_class
supportFragmentManager.switch(
newFrag = ClassesPageFragment.newInstance(),
tag = getString(R.string.classes)
)
}
getString(R.string.songs) -> {
bottom_navigation.selectedItemId = R.id.nav_song
supportFragmentManager.switch(
newFrag = SongsPageFragment.newInstance(),
tag = getString(R.string.songs)
)
}
getString(R.string.play_song) -> {
if(!model.name.isNullOrBlank()) {
songPosition = position
mSongList = songList
MyLog.e(TAG, "======= ${getString(R.string.play_song)} ${model.name}")
isPlaying = false // to start new song
// play song
if (!lnPlayer.isVisible) {
lnPlayer.isVisible = true
}
titleTxt.setHtmlText(model.name)
if(mBound) {
startService()
mMusicPlayerService.playSong(mSongList,songPosition)
media_button.setImageResource(R.drawable.ic_pause_black_24dp)
}
setPlayPause(!mMusicPlayerService.isPlaying())
initSeekBar()
}
}
else -> {
}
}
}
private val TAG = javaClass.simpleName
private var isPlaying = false
private var mediaSource: ProgressiveMediaSource? = null
// notification
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var mediaSession: MediaSessionCompat
private lateinit var playerNotificationManager: PlayerNotificationManager
private var handler: Handler? = null
private val dashUrl = "http://www.panacherock.com/downloads/mp3/01_All_Tangled_Up.mp3"
var mSongList: ArrayList<RecentSong> = ArrayList()
var songPosition = 0
val MESSAGE_KEY = "message_key"
private lateinit var mMusicPlayerService:MusicPlayerService
private lateinit var mPlayer: SimpleExoPlayer
private var mBound = false
private val mServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) {
val binder = iBinder as MusicPlayerService.MyServiceBinder
mMusicPlayerService = binder.getService()
mPlayer = mMusicPlayerService.getPlayerInstance()!!
mPlayer.addListener(playerListener)
mBound = true
MyLog.e(TAG,"onServiceConnected: ")
}
override fun onServiceDisconnected(componentName: ComponentName) {
mBound = false
MyLog.e(TAG,"onServiceDisconnected: ") // calles only in rare case if service destroy unexpectedly
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (savedInstanceState == null) {
supportFragmentManager.switch(
newFrag = DashboardPageFragment.newInstance(),
tag = getString(R.string.home)
)
}
getIntentData()
bottom_navigation.setOnNavigationItemSelectedListener { item ->
hideKeybord()
when(item.itemId) {
R.id.nav_home -> {
checkFragment()
supportFragmentManager.switch(
newFrag = DashboardPageFragment.newInstance(),
tag = getString(R.string.home)
)
true
}
R.id.nav_song -> {
checkFragment()
supportFragmentManager.switch(
newFrag = SongsPageFragment.newInstance(),
tag = getString(R.string.songs)
)
true
}
R.id.nav_speaker -> {
checkFragment()
supportFragmentManager.switch(
newFrag = SpeakerPageFragment.newInstance(),
tag = getString(R.string.speakers)
)
true
}
R.id.nav_class -> {
checkFragment()
supportFragmentManager.switch(
newFrag = ClassesPageFragment.newInstance(),
tag = getString(R.string.classes)
)
true
}
R.id.nav_search -> {
checkFragment()
supportFragmentManager.switch(
newFrag = SearchPageFragment.newInstance(),
tag = getString(R.string.search)
)
true
}
else -> false
}
}
bottom_navigation.setOnNavigationItemReselectedListener { } //disable reselection tab
media_button.setOnClickListener {
// startService()
setPlayPause(!isPlaying)
}
imgClose.setOnClickListener {
isPlaying = true // to stop song
playerNotificationManager.setPlayer(null)
setPlayPause(!isPlaying)
if (lnPlayer.isVisible) {
lnPlayer.isVisible = false
}
}
}
private fun getIntentData() {
if (intent!=null && intent.hasExtra("internet") && !intent.getBooleanExtra("internet", false)) {
this.changeFragment(
DownloadPageFragment.newInstance(), Constants.DOWNLOAD_TAG
)
}
}
private fun checkFragment() {
val fragment = getSupportFragmentManager().findFragmentByTag(Constants.SUB_CATEGORY_TAG)
if (fragment != null) {
getSupportFragmentManager().beginTransaction().remove(fragment).commit()
}
val fragment2 = getSupportFragmentManager().findFragmentByTag(Constants.DOWNLOAD_TAG)
if (fragment2 != null) {
getSupportFragmentManager().beginTransaction().remove(fragment2).commit()
}
}
private val playerListener by lazy {
object : Player.EventListener {
override fun onPlayerError(error: ExoPlaybackException) {
super.onPlayerError(error)
//onError(error)
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
when (playbackState) {
Player.STATE_BUFFERING -> Log.e(TAG,"STATE_BUFFERING")
Player.STATE_ENDED -> Log.e(TAG,"STATE_ENDED")
Player.STATE_IDLE -> Log.e(TAG,"STATE_IDLE")
Player.STATE_READY -> {
// setPlayPause(playWhenReady)
isPlaying = playWhenReady
mPlayer.setPlayWhenReady(playWhenReady)
if (!isPlaying) {
media_button.setImageResource(R.drawable.ic_play_arrow_black_24dp)
} else {
setProgress()
media_button.setImageResource(R.drawable.ic_pause_black_24dp)
}
if (playWhenReady) {
Log.e(TAG, "PlaybackStatus.PLAYING")
} else {
Log.e(TAG, "PlaybackStatus.PAUSED")
}
titleTxt.setHtmlText(mSongList[mPlayer.currentWindowIndex].name)
MyLog.e(TAG, "====== " + mSongList[mPlayer.currentWindowIndex].name)
MyLog.e(TAG, "======111 " + mSongList[mPlayer.currentWindowIndex].descripation)
}
else -> Log.e(TAG, "PlaybackStatus.IDLE")
}
}
}
}
private fun startService() {
val myService = Intent(this, MusicPlayerService ::class.java)
Util.startForegroundService(this,myService)
}
private fun stopService() {
val myService = Intent(this, MusicPlayerService ::class.java)
stopService(myService)
}
private fun setPlayPause(play: Boolean) {
if(mBound) {
if(mMusicPlayerService.isPlaying()) {
mMusicPlayerService.pause()
media_button.setImageResource(R.drawable.ic_play_arrow_black_24dp)
} else {
startService()
playerNotificationManager = mMusicPlayerService.getNotificationInstance()!!
/* if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}*/
mMusicPlayerService.play()
setProgress()
media_button.setImageResource(R.drawable.ic_pause_black_24dp)
}
}
/*isPlaying = play
exoPlayer.setPlayWhenReady(play)
if (!isPlaying) {
media_button.setImageResource(R.drawable.ic_play_arrow_black_24dp)
} else {
setProgress()
media_button.setImageResource(R.drawable.ic_pause_black_24dp)
}*/
}
private fun stringForTime(timeMs: Int): String {
val mFormatBuilder: StringBuilder
val mFormatter: Formatter
mFormatBuilder = StringBuilder()
mFormatter = Formatter(mFormatBuilder, Locale.getDefault())
val totalSeconds = timeMs / 1000
val seconds = totalSeconds % 60
val minutes = totalSeconds / 60 % 60
val hours = totalSeconds / 3600
mFormatBuilder.setLength(0)
return if (hours > 0) {
mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
} else {
mFormatter.format("%02d:%02d", minutes, seconds).toString()
}
}
private fun setProgress() {
seekPlayerProgress.progress = 0
seekPlayerProgress.max = mPlayer.getDuration().toInt() / 1000
position.setText(stringForTime(mPlayer.getCurrentPosition().toInt()))
duration.setText(stringForTime(mPlayer.getDuration().toInt()))
if (handler == null) handler = Handler()
//Make sure you update Seekbar on UI thread
handler!!.post(object : Runnable {
override fun run() {
if (mPlayer != null && ::mPlayer.isInitialized && isPlaying) {
seekPlayerProgress.max = mPlayer.getDuration().toInt() / 1000
val mCurrentPosition = mPlayer.getCurrentPosition().toInt() / 1000
seekPlayerProgress.progress = mCurrentPosition
position.setText(stringForTime(mPlayer.getCurrentPosition().toInt()))
duration.setText(stringForTime(mPlayer.getDuration().toInt()))
handler!!.postDelayed(this, 1000)
}
}
})
}
private fun initSeekBar() {
seekPlayerProgress.requestFocus()
seekPlayerProgress.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (!fromUser) {
// We're not interested in programmatically generated changes to
// the progress bar's position.
return
}
mPlayer.seekTo((progress * 1000).toLong())
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
})
seekPlayerProgress.setMax(0)
seekPlayerProgress.setMax(mPlayer.getDuration().toInt() / 1000)
}
private var doubleBackToExitPressedOnce = false
override fun onBackPressed() {
val fragment = getSupportFragmentManager().findFragmentByTag(Constants.SUB_CATEGORY_TAG)
if (fragment != null) {
getSupportFragmentManager().beginTransaction().remove(fragment).commit()
return
}
val fragment2 = getSupportFragmentManager().findFragmentByTag(Constants.DOWNLOAD_TAG)
if (fragment2 != null) {
getSupportFragmentManager().beginTransaction().remove(fragment2).commit()
return
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return
}
this.doubleBackToExitPressedOnce = true
this.showSnackBarToast("Please click BACK again to exit")
Handler().postDelayed(Runnable { doubleBackToExitPressedOnce = false }, 2000)
}
override fun onDestroy() {
if(::playerNotificationManager.isInitialized) {
playerNotificationManager.setPlayer(null)
}
killPlayer()
// stopService()
MyLog.e(TAG," #### onDestroy #### ")
super.onDestroy()
}
override fun onStop() {
super.onStop()
if(mBound) {
unbindService(mServiceConnection)
mBound = false
}
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(broadCastReceiver)
MyLog.e(TAG," #### onStop #### ")
}
private fun killPlayer() {
if (mPlayer != null) {
mPlayer.release()
mediaSource = null
}
MyLog.e(TAG,"#### killPlayer ####")
}
private fun getHtmlText(str:String):String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return (Html.fromHtml(str, Html.FROM_HTML_MODE_COMPACT)).toString()
} else {
return (Html.fromHtml(str)).toString()
}
}
private fun setMediaSession() {
mediaSession = MediaSessionCompat(this#MainActivity,"MEDIA_SESSION_TAG")
mediaSession.isActive = true
playerNotificationManager.setMediaSessionToken(mediaSession.sessionToken) //enhance media stye notification and provide artwork in lock screen
mediaSessionConnector = MediaSessionConnector(mediaSession)
//timeline is the internal representation of the pllaylist after the player has been prepared
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession){
override fun getMediaDescription(player: Player?, windowIndex: Int): MediaDescriptionCompat {
return getMediaDescriptionData(this#MainActivity, mSongList[windowIndex])
}
})
mediaSessionConnector.setPlayer(exoPlayer/*, null*/) //sync player wth media session
}
fun getMediaDescriptionData(context: Context, sample: RecentSong): MediaDescriptionCompat {
val extras = Bundle()
val options = BitmapFactory.Options()
options.inSampleSize = 8
// val bitmap = BitmapFactory.decodeFile(getBitmap(context, sample.bitmapResource), options)
val bitmap = context.getBitmap(R.drawable.bg)
extras.putParcelable(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
extras.putParcelable(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap)
return MediaDescriptionCompat.Builder()
.setMediaId(sample.song)
.setIconBitmap(bitmap)
.setTitle(sample.name)
.setDescription(sample.descripation)
.setExtras(extras)
.build()
}
override fun onStart() {
super.onStart()
bindService(Intent(this,MusicPlayerService::class.java),mServiceConnection,Context.BIND_AUTO_CREATE)
LocalBroadcastManager.getInstance(this)
.registerReceiver(broadCastReceiver, IntentFilter(MusicPlayerService().MUSIC_COMPLETED))
}
val broadCastReceiver = object : BroadcastReceiver() {
override fun onReceive(contxt: Context?, intent: Intent?) {
MyLog.e(TAG,"onReceive: ${intent?.getBooleanExtra(MESSAGE_KEY,false)}")
if(intent!!.getBooleanExtra(MESSAGE_KEY,false)) {
setPlayPause(intent.getBooleanExtra(MESSAGE_KEY,false))
if (intent.getBooleanExtra(MESSAGE_KEY,false)) {
Log.e(TAG, "PlaybackStatus.PLAYING")
} else {
Log.e(TAG, "PlaybackStatus.PAUSED")
}
titleTxt.setHtmlText(mSongList[mPlayer.currentWindowIndex].name)
MyLog.e(TAG,"====== "+mSongList[mPlayer.currentWindowIndex].name)
MyLog.e(TAG,"======111 "+mSongList[mPlayer.currentWindowIndex].descripation)
}
}
}
}
MusicPlayerService
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Binder
import android.os.IBinder
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.brahmakumaris.MainActivity
import com.brahmakumaris.R
import com.brahmakumaris.model.getDashboard.RecentSong
import com.brahmakumaris.util.MyLog
import com.brahmakumaris.util.getBitmap
import com.brahmakumaris.util.getPlaintextfromHtmlHtmlText
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
class MusicPlayerService : Service() {
private val TAG = javaClass.simpleName
private val mContext: Context = this
private val mBinder = MyServiceBinder()
val MUSIC_COMPLETED = "music completed"
val mPlayer: SimpleExoPlayer by lazy { SimpleExoPlayer.Builder(this).build() }
private lateinit var playerNotificationManager: PlayerNotificationManager
var mSongList: ArrayList<RecentSong> = ArrayList()
override fun onCreate() {
super.onCreate()
MyLog.d(TAG,"onCreate: ")
}
fun playSong(mSongList: ArrayList<RecentSong> = ArrayList(), position: Int = 0)/* mContext: Context = this*/ {
this.mSongList = mSongList
val dataSourceFactory = DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext,getString(R.string.app_name)))
val concateMediaSource = ConcatenatingMediaSource()
for (i in mSongList) {
val mediaSource = ProgressiveMediaSource
.Factory(
DefaultDataSourceFactory(mContext, dataSourceFactory),
DefaultExtractorsFactory()
)
.createMediaSource(/*i.uri*/Uri.parse(i.musicFile))
concateMediaSource.addMediaSource(mediaSource)
}
mPlayer.prepare(concateMediaSource)
mPlayer.seekToDefaultPosition(position)
mPlayer.playWhenReady =true
setNotification()
}
private fun setNotification() {
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
this,"channel_id",R.string.channelName,R.string.channelDescription,11,
object : PlayerNotificationManager.MediaDescriptionAdapter{
override fun createCurrentContentIntent(player: Player): PendingIntent? {
val intent = Intent(mContext, MainActivity::class.java)
return PendingIntent.getActivity(mContext,0,intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
override fun getCurrentContentText(player: Player): String? {
// return mSongList[player.currentWindowIndex].descripation // descrption
return mSongList[player.currentWindowIndex].descripation.getPlaintextfromHtmlHtmlText() // descrption
}
override fun getCurrentContentTitle(player: Player): String {
// return mSongList[player.currentWindowIndex].name // title
return mSongList[player.currentWindowIndex].name.getPlaintextfromHtmlHtmlText()
}
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
return mContext.getBitmap(R.drawable.bg)
}
} ,object : PlayerNotificationManager.NotificationListener {
override fun onNotificationStarted(notificationId: Int, notification: Notification) {
startForeground(notificationId,notification)
}
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
startForeground(notificationId,notification)
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
stopSelf()
}
override fun onNotificationCancelled(notificationId: Int) {
stopSelf()
}
}
)
//show hide button
playerNotificationManager.setUseStopAction(false) //stop song
playerNotificationManager.setRewindIncrementMs(0) //hide rewind button
playerNotificationManager.setFastForwardIncrementMs(0) //hide fast forward button
playerNotificationManager.setPlayer(mPlayer)
}
inner class MyServiceBinder : Binder() {
fun getService(): MusicPlayerService = this#MusicPlayerService
}
fun getPlayerInstance(): SimpleExoPlayer? {
return mPlayer
}
fun getNotificationInstance(): PlayerNotificationManager? {
return playerNotificationManager
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
MyLog.d(TAG,"onStartCommand: ")
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
MyLog.d(TAG,"onBind: ")
return mBinder
}
override fun onUnbind(intent: Intent?): Boolean {
MyLog.d(TAG,"onUnbind: ")
return true
}
override fun onRebind(intent: Intent?) {
MyLog.d(TAG,"onRebind: ")
super.onRebind(intent)
}
override fun onDestroy() {
MyLog.e(TAG,"onDestroy: ")
super.onDestroy()
mPlayer.release()
if(::playerNotificationManager.isInitialized) {
playerNotificationManager.setPlayer(null)
}
}
// public client method
fun isPlaying():Boolean {
return mPlayer.isPlaying
}
fun play() {
mPlayer.setPlayWhenReady(true)
}
fun pause() {
mPlayer.setPlayWhenReady(false)
}
}
if I minimize the app then it's printing unbind and then it directly going to onDestroy why it's happening can anyone help me,
Any Help Would Be Highly Appreciated.
I just remove onStop unbind and unregister code and set in onDestroy and it's working for me.
You should be using startForeground() with a notification, to Android service that never stops running.
startForeground(1, notification)
Please have a look at this.
https://robertohuertas.com/2019/06/29/android_foreground_services/
I hope this will help you.
I experienced this issue because I set the player on notification manager to null on onStop which causes onNotificationCancelled to be called, you should check, if the notification was canceled by the user then you should stop the service, else leave it running.
override fun onNotificationCancelled(notificationId: Int, dismissedByUser:Boolean)
{
if (dismissedByUser) {
stopSelf()
}
}

BLE - Bluetooth GATT service can't close connection

I have implemented BLE and it works. The flow is: DrawerActivity starts, it sets a fragmentA, which has BLE implementation, because I want active BLE only in fragmentA. So if you switch to fragmentB it should terminate the BLE connection and upair the device.
What happens is that the only time it completely disconnects is, when you close the app, or turn off the bluetooth. If you close the fragmentA and open it again it works from drawerActivity. If you do it again, so this is now the 3rd time, it won't pair to the BLE device. When I investigated further, it won't even find the correct BLE device.. Meaning if you run the fragment the 4th, 5th time it is the same result.
What I want to achieve is when onDestroy in Fragment is called it should disconnect from the BLE and destroy all references. And then if you go into the fragmentA again it should recreate everything again, no matter how many times you open the fragmentA.But now the device isn't found anymore, probably because it didn't disconnect properly and BLE device has old references or something.
This is how I disconnect.
This is onDestroy method:
override fun onDestroy() {
super.onDestroy()
activity?.unregisterReceiver(bluetoothReceiver)
bluetoothManager?.disconnectBluetoothService()
bluetoothManager = null
}
And in bluetoothManager
fun disconnectBluetoothService() {
bluetoothService?.disconnectGattServer()
}
And at the bluetoothService:
fun disconnectGattServer() {
mConnected = false
mBluetoothGatt?.disconnect()
mBluetoothGatt?.close()
mBluetoothGatt = null
}
Here are the all 3 files that are used for BLE.
FragmentA
private var bluetoothManager: MyBluetoothManager? = null
private val bluetoothReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_OFF -> {}
BluetoothAdapter.STATE_ON -> {
initBluetoothIfPossible()
bluetoothManager?.scanForBluetoothDevicesIfPossible(true)
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenToBluetoothChanges()
}
override fun onDestroy() {
super.onDestroy()
activity?.unregisterReceiver(bluetoothReceiver)
bluetoothManager?.disconnectBluetoothService()
bluetoothManager = null
}
private fun listenToBluetoothChanges() {
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
carSharingActivity?.registerReceiver(bluetoothReceiver, filter)
}
private fun initBluetoothIfPossible() {
bluetoothToken ?: return
if (bluetoothManager != null) {
bluetoothManager!!.pairDevice()
} else {
bluetoothManager = MyBluetoothManager(activity as Activity,
this,
bluetoothToken!!.token,
bluetoothToken!!.sessionKey,
bluetoothToken!!.uuid)
}
setImageForBluetoothStatus()
}
MyBluetoothManager
class ACCarBluetoothManager(var activity: Activity,
var listener: MyBluetoothListener,
private var token: String,
private var sessionKey: String,
private var accessDeviceUID: String) {
// Bluetooth adapter
private var bluetoothAdapter: BluetoothAdapter?
// Bluetooth service
private var bluetoothService: MyBluetoothService? = null
private var isBluetoothAvailable: Boolean = false
val isBluetoothEnabled: Boolean
get() = bluetoothAdapter?.isEnabled == true
var connectionStatus: Boolean = false
set(value) {
if (field == value) return
field = value
if (value) stopScanning()
else startScanning()
}
private var savedDevice: BluetoothDevice? = null
/**
* Service lifecyle management.
*/
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(componentName: ComponentName, service: IBinder) {
bluetoothService = (service as MyBluetoothService.LocalBinder).service
bluetoothService?.isConnectedListener = { isConnected ->
listener.isConnected(isConnected)
connectionStatus = isConnected
}
isBluetoothAvailable = bluetoothService?.initialize() == true
}
override fun onServiceDisconnected(componentName: ComponentName) {
bluetoothService = null
connectionStatus = false
}
}
/**
* Broadcast receiver.
*/
private val gattUpdateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
try {
when (intent.action) {
BluetoothConstants.ACTION_GATT_SERVICES_DISCOVERED -> bluetoothService?.initializeIndications()
BluetoothConstants.ACTION_INDICATIONS_INITIALIZED -> bluetoothService?.startAuthentication(token)
}
} catch (e: Exception) {
Log.e("GattUpdateReciever", e.message)
}
}
}
/**
* Bluetooth device scanning callback. The scanned device is added to the list of available
* devices.
*/
private val bluetoothScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
val btDevice = result.device
if (btDevice.name.isNullOrEmpty()) return
if (deviceMatchesUID(btDevice)) {
savedDevice = btDevice
pairDevice()
}
}
}
init {
val gattServiceIntent = Intent(activity, MyBluetoothService::class.java)
activity.bindService(gattServiceIntent, this.serviceConnection, Context.BIND_AUTO_CREATE)
// Setup bluetooth adapter
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
// If bluetooth is not enabled, request permission, otherwise start scanning process, Not IMPLEMENTED, because it is not needed.
scanForBluetoothDevicesIfPossible()
activity.registerReceiver(gattUpdateReceiver, BluetoothConstants.makeGattUpdateIntentFilter())
}
fun scanForBluetoothDevicesIfPossible(enable: Boolean = isBluetoothEnabled) {
val hasLocationPermission = ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
if (enable) {
if (hasLocationPermission) {
startScanning()
}
//You can request for location permission if he doesn't have permission
} else {
stopScanning()
}
}
fun pairDevice() {
if (isBluetoothAvailable && savedDevice != null) {
bluetoothService?.connect(savedDevice!!)
}
}
fun startScanning() {
bluetoothAdapter?.bluetoothLeScanner?.startScan(bluetoothScanCallback)
}
fun stopScanning() {
bluetoothAdapter?.bluetoothLeScanner?.stopScan(bluetoothScanCallback)
}
fun deviceMatchesUID(device: BluetoothDevice): Boolean {
return device.name.equals(accessDeviceUID, ignoreCase = true)
}
}
MyBluetoothService
class ACCarBluetoothService : Service() {
var isConnectedListener: ((Boolean) -> Unit)? = null
var mConnected = false
set(value) {
field = value
isConnectedListener?.invoke(value)
}
private val mBinder = LocalBinder()
private var mBluetoothManager: BluetoothManager? = null
private var mBluetoothAdapter: BluetoothAdapter? = null
private var mBluetoothGatt: BluetoothGatt? = null
private var mDividedTokenList: MutableList<ByteArray>? = null
// Various callback methods defined by the BLE API.
private val mGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (status == BluetoothGatt.GATT_FAILURE
|| status != BluetoothGatt.GATT_SUCCESS
|| newState == BluetoothProfile.STATE_DISCONNECTED) {
disconnectGattServer()
return
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) onServiceDiscoveryReady()
}
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
when {
descriptor.characteristic.uuid == BluetoothConstants.UUID_COMMAND_CHALLENGE -> setCharacteristicNotification(
BluetoothConstants.UUID_DEBUG,
true)
descriptor.characteristic.uuid == BluetoothConstants.UUID_DEBUG -> setCharacteristicNotification(
BluetoothConstants.UUID_STATUS_1,
true)
descriptor.characteristic.uuid == BluetoothConstants.UUID_STATUS_1 -> setCharacteristicNotification(
BluetoothConstants.UUID_STATUS_2,
true)
descriptor.characteristic.uuid == BluetoothConstants.UUID_STATUS_2-> setCharacteristicNotification(
BluetoothConstants.UUID_STATUS_3,
true)
else -> onIndicationsInitialized()
}
}
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) broadcastUpdate(characteristic)
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
if (characteristic.uuid == BluetoothConstants.UUID_COMMAND_CHALLENGE) {
commandChallenge = characteristic.value
} else {
broadcastUpdate(characteristic)
}
}
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (BluetoothConstants.UUID_AUTHORIZE_PHONE == characteristic.uuid) writeNextPartToken()
}
}
override fun onBind(intent: Intent): IBinder? {
return mBinder
}
/**
* Initializes a reference to the local Bluetooth adapter.
*
* #return Return true if the initialization is successful.
*/
fun initialize(): Boolean {
if (mBluetoothManager == null) {
mBluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (mBluetoothManager == null) return false
}
mBluetoothAdapter = mBluetoothManager!!.adapter
if (mBluetoothAdapter == null) return false
return true
}
fun initializeIndications() {
setCharacteristicNotification(BluetoothConstants.UUID_COMMAND_CHALLENGE, true)
}
fun startAuthentication(token: String) {
mDividedTokenList = Tools.divideArray(Tools.decodeBase64(token))
writeNextPartToken()
}
fun writeCommand(sessionKey: String, command: ByteArray) {
val safeCommand = Tools.generateSafeCommand(command, commandChallenge, Tools.decodeBase64(sessionKey))
val commandCharacteristic = mBluetoothGatt!!.getService(BluetoothConstants.UUID_CAR_CONTROL_SERVICE)
.getCharacteristic(BluetoothConstants.UUID_COMMAND_PHONE)
commandCharacteristic.value = safeCommand
mBluetoothGatt!!.writeCharacteristic(commandCharacteristic)
}
fun connect(device: BluetoothDevice) {
mBluetoothGatt = device.connectGatt(this, false, this.mGattCallback)
}
fun disconnectGattServer() {
mConnected = false
mBluetoothGatt?.disconnect()
mBluetoothGatt?.close()
mBluetoothGatt = null
}
private fun onIndicationsInitialized() {
val intent = Intent()
intent.action = BluetoothConstants.ACTION_INDICATIONS_INITIALIZED
sendBroadcast(intent)
}
private fun onServiceDiscoveryReady() {
val intent = Intent()
intent.action = BluetoothConstants.ACTION_GATT_SERVICES_DISCOVERED
sendBroadcast(intent)
}
private fun writeNextPartToken() {
if (mDividedTokenList!!.isEmpty()) {
broadcastUpdate(BluetoothConstants.ACTION_INIT_READY)
return
}
writeValue(BluetoothConstants.UUID_AUTHORIZE_PHONE, mDividedTokenList!!.removeAt(0))
}
private fun broadcastUpdate(action: String) {
val intent = Intent(action)
sendBroadcast(intent)
}
private fun writeValue(characteristicUUID: UUID, valueBytes: ByteArray) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return
val service = mBluetoothGatt!!.getService(BluetoothConstants.UUID_CAR_CONTROL_SERVICE)
val characteristic = service.getCharacteristic(characteristicUUID)
characteristic.value = valueBytes
mBluetoothGatt!!.writeCharacteristic(characteristic)
}
private fun setCharacteristicNotification(characteristicUUID: UUID, enabled: Boolean) {
if (mBluetoothAdapter == null || mBluetoothGatt == null) return
val characteristic = mBluetoothGatt!!
.getService(BluetoothConstants.UUID_CAR_INFORMATION_SERVICE)
.getCharacteristic(characteristicUUID)
mBluetoothGatt!!.setCharacteristicNotification(characteristic, enabled)
characteristic.getDescriptor(CONFIG_DESCRIPTOR)?.let {
it.value = if (enabled) BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
mBluetoothGatt!!.writeDescriptor(it)
}
}
private fun broadcastUpdate(characteristic: BluetoothGattCharacteristic) {
val intent = Intent()
if (BluetoothConstants.UUID_STATUS_1 == characteristic.uuid) {
if (!hasDataInBluetooth(characteristic.value)) {
mConnected = true
statusListener?.invoke()
}
intent.action = BluetoothConstants.ACTION_STATUS_1_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
if (BluetoothConstants.UUID_DEBUG == characteristic.uuid) {
intent.action = BluetoothConstants.ACTION_DEBUG_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
if (BluetoothConstants.UUID_STATUS_2 == characteristic.uuid) {
intent.action = BluetoothConstants.ACTION_STATUS_1_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
if (BluetoothConstants.UUID_STATUS_3 == characteristic.uuid) {
intent.action = BluetoothConstants.ACTION_STATUS_1_AVAILABLE
intent.putExtra(BluetoothConstants.EXTRA_DATA, characteristic.value)
}
sendBroadcast(intent)
}
private fun hasDataInBluetooth(byteArray: ByteArray): Boolean {
for (b in byteArray) {
if (b.toInt() != 0) {
return false
}
}
return true
}
inner class LocalBinder : Binder() {
val service: MyBluetoothService
get() = this#MyBluetoothService
}
}
I found the solution.
The problem vas in:
fun pairDevice() {
if (isBluetoothAvailable && savedDevice != null) {
bluetoothService?.connect(savedDevice!!)
}
}
Because it was trying to connect again and again it stopped broadcasting.
I solved it with:
fun pairDevice() {
if (isConnected) return
if (isBluetoothAvailable && savedDevice != null) {
bluetoothService?.connect(savedDevice!!)
isConnected = true
}
}

Android NsdManager not able to discover services

I'm running into a problem with Androids NsdManager when following their tutorial Using Network Service Discovery.
I have a few zeroconf/bonjour hardware devices on my network. From my mac I can discover all of them as expected from my terminal with the following
dns-sd -Z _my-mesh._tcp.
From my Android app's first run I can flawlessly discover these services using NsdManager. However if I restart the application and try again none of the services are found. onDiscoveryStarted gets called successfully but then nothing else after. While waiting I can confirm from my mac that the services are still successfully there.
I can then turn on my Zeroconf app (on Android) and it will show the services like my mac. When I return to my app I see it immediately receive all the callbacks I expected previously. So I believe something is wrong with my approach, however I'm not sure what. Below is the code I use to discover and resolve services. The view is a giant textview (in a scroll view) I keep writing text to for debugging easier.
import android.annotation.SuppressLint
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class MainActivity : AppCompatActivity(),
NsdManager.DiscoveryListener {
private var nsdManager: NsdManager? = null
private var text: TextView? = null
private var isResolving = false
private val services = ArrayList<ServiceWrapper>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
this.text = findViewById(R.id.text)
this.nsdManager = application.getSystemService(Context.NSD_SERVICE) as NsdManager
}
override fun onResume() {
super.onResume()
this.nsdManager?.discoverServices("_my-mesh._tcp.", NsdManager.PROTOCOL_DNS_SD, this)
write("Resume Discovering Services")
}
override fun onPause() {
super.onPause()
this.nsdManager?.stopServiceDiscovery(this)
write("Pause Discovering Services")
}
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
write("onServiceFound(serviceInfo = $serviceInfo))")
if (serviceInfo == null) {
return
}
add(serviceInfo)
}
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
write("onStopDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
}
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
write("onStartDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
}
override fun onDiscoveryStarted(serviceType: String?) {
write("onDiscoveryStarted(serviceType = $serviceType)")
}
override fun onDiscoveryStopped(serviceType: String?) {
write("onDiscoveryStopped(serviceType = $serviceType)")
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
write("onServiceLost(serviceInfo = $serviceInfo)")
}
private fun createResolveListener(): NsdManager.ResolveListener {
return object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
write("onResolveFailed(serviceInfo = $serviceInfo, errorCode = $errorCode)")
isResolving = false
resolveNext()
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
write("onServiceResolved(serviceInfo = $serviceInfo)")
if (serviceInfo == null) {
return
}
for (servicewrapper in services) {
if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
servicewrapper.resolve(serviceInfo)
}
}
isResolving = false
resolveNext()
}
}
}
#SuppressLint("SetTextI18n")
private fun write(text: String?) {
this.text?.let {
it.post({
it.text = it.text.toString() + "\n" + text + "\n"
})
}
}
fun add(serviceInfo: NsdServiceInfo) {
for (servicewrapper in services) {
if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
return
}
}
services.add(ServiceWrapper(serviceInfo))
resolveNext()
}
#Synchronized
fun resolveNext() {
if (isResolving) {
return
}
isResolving = true
for (servicewrapper in services) {
if (servicewrapper.isResolved) {
continue
}
write("resolving")
this.nsdManager?.resolveService(servicewrapper.serviceInfo, createResolveListener())
return
}
isResolving = false
}
inner class ServiceWrapper(var serviceInfo: NsdServiceInfo) {
var isResolved = false
fun resolve(serviceInfo: NsdServiceInfo) {
isResolved = true
this.serviceInfo = serviceInfo
}
}
}
Better late than never. Did not realize other people were having this issue too until now.
What we discovered was some routers were blocking or not correctly forwarding the packets back and forth. Our solution to this was using wire shark to detect what other popular apps were doing to get around the issue. Androids NsdManager has limited customizability so it required manually transmitting the packet over a MulticastSocket.
interface NsdDiscovery {
suspend fun startDiscovery()
suspend fun stopDiscovery()
fun setListener(listener: Listener?)
fun isDiscovering(): Boolean
interface Listener {
fun onServiceFound(ip:String, local:String)
fun onServiceLost(event: ServiceEvent)
}
}
#Singleton
class ManualNsdDiscovery #Inject constructor()
: NsdDiscovery {
//region Fields
private val isDiscovering = AtomicBoolean(false)
private var socketManager: SocketManager? = null
private var listener: WeakReference<NsdDiscovery.Listener> = WeakReference<NsdDiscovery.Listener>(null)
//endregion
//region NsdDiscovery
override suspend fun startDiscovery() = withContext(Dispatchers.IO) {
if (isDiscovering()) return#withContext
this#ManualNsdDiscovery.isDiscovering.set(true)
val socketManager = SocketManager()
socketManager.start()
this#ManualNsdDiscovery.socketManager = socketManager
}
override suspend fun stopDiscovery() = withContext(Dispatchers.IO) {
if (!isDiscovering()) return#withContext
this#ManualNsdDiscovery.socketManager?.stop()
this#ManualNsdDiscovery.socketManager = null
this#ManualNsdDiscovery.isDiscovering.set(false)
}
override fun setListener(listener: NsdDiscovery.Listener?) {
this.listener = WeakReference<NsdDiscovery.Listener>(listener)
}
#Synchronized
override fun isDiscovering(): Boolean {
return this.isDiscovering.get()
}
//endregion
private inner class SocketManager {
//region Fields
private val group = InetAddress.getByName("224.0.0.251")
?: throw IllegalStateException("Can't setup group")
private val incomingNsd = IncomingNsd()
private val outgoingNsd = OutgoingNsd()
//endregion
//region Constructors
//endregion
//region Methods
suspend fun start() {
this.incomingNsd.startListening()
this.outgoingNsd.send()
}
fun stop() {
this.incomingNsd.stopListening()
}
//endregion
private inner class OutgoingNsd {
//region Fields
private val socketMutex = Mutex()
private var socket = MulticastSocket(5353)
suspend fun setUpSocket() {
this.socketMutex.withLock {
try {
this.socket = MulticastSocket(5353)
this.socket.reuseAddress = true
this.socket.joinGroup(group)
} catch (e: SocketException) {
return
}
}
}
suspend fun tearDownSocket() {
this.socketMutex.withLock {
this#OutgoingNsd.socket.close()
}
}
//ugly code but here is the packet
private val bytes = byteArrayOf(171.toByte(), 205.toByte(), 1.toByte(), 32.toByte(),
0.toByte(), 1.toByte(), 0.toByte(), 0.toByte(),
0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(),
9.toByte(), 95.toByte(), 101.toByte(), 118.toByte(),
97.toByte(), 45.toByte(), 109.toByte(), 101.toByte(),
115.toByte(), 104.toByte(), 4.toByte(), 95.toByte(),
116.toByte(), 99.toByte(), 112.toByte(), 5.toByte(),
108.toByte(), 111.toByte(), 99.toByte(), 97.toByte(),
108.toByte(), 0.toByte(), 0.toByte(), 12.toByte(),
0.toByte(), 1.toByte())
private val outPacket = DatagramPacket(bytes,
bytes.size,
this#SocketManager.group,
5353)
//endregion
//region Methods
#Synchronized
suspend fun send() {
withContext(Dispatchers.Default) {
setUpSocket()
try {
this#OutgoingNsd.socket.send(this#OutgoingNsd.outPacket)
delay(1500L)
tearDownSocket()
} catch (e: Exception) {
}
}
}
//endregion
}
private inner class IncomingNsd {
//region Fields
private val isRunning = AtomicBoolean(false)
private var socket = MulticastSocket(5353)
//endregion
//region Any
fun setUpSocket() {
try {
this.socket = MulticastSocket(5353)
this.socket.reuseAddress = true
this.socket.joinGroup(group)
} catch (e: SocketException) {
} catch (e: BindException) {
}
}
fun run() {
GlobalScope.launch(Dispatchers.Default) {
setUpSocket()
try {
while (this#IncomingNsd.isRunning.get()) {
val bytes = ByteArray(4096)
val inPacket = DatagramPacket(bytes, bytes.size)
this#IncomingNsd.socket.receive(inPacket)
val incoming = DNSIncoming(inPacket)
for (answer in incoming.allAnswers) {
if (answer.key.contains("_my_mesh._tcp")) {
this#ManualNsdDiscovery.listener.get()?.onServiceFound(answer.recordSource.hostAddress, answer.name)
return#launch
}
}
}
this#IncomingNsd.socket.close()
} catch (e: Exception) {
}
}
}
//endregion
//region Methods
#Synchronized
fun startListening() {
if (this.isRunning.get()) {
return
}
this.isRunning.set(true)
run()
}
#Synchronized
fun stopListening() {
if (!this.isRunning.get()) {
return
}
this.isRunning.set(false)
}
//endregion
}
}
}

Android: How to detect Bluetooth connection status

I want to detect the connection status of a paired Bluetooth headset
to the phone.
In Android 3.0 (API level 11) "BluetoothHeadset" class has
"isAudioConnected()" method.
I don't know how to create (initialize) a "BluetoothHeadset" object.
It seems that I need to use
"getProfileProxy ()" but I need a sample code to find out how I need
to create and pass the parameters.
Thanks,
Hos
You need to implement BluetoothProfile.ServiceListener :
BluetoothProfile.ServiceListener b = new BlueToothListener();
boolean profileProxy = BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(Handler.bot, b, BluetoothProfile.HEADSET);
public class BlueToothListener implements ServiceListener {
public static BluetoothHeadset headset;
public static BluetoothDevice bluetoothDevice;
#Override
public void onServiceDisconnected(int profile) {// dont care
headset = null;
}
#Override
public void onServiceConnected(int profile,
BluetoothProfile proxy) {// dont care
try {
Debugger.test("BluetoothProfile onServiceConnected "+proxy);
if (proxy instanceof BluetoothHeadset)
headset = ((BluetoothHeadset) proxy);
else// getProfileProxy(Handler.bot, b, BluetoothProfile.HEADSET);
return;// ^^ => NEVER
List<BluetoothDevice> connectedDevices = proxy
.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Debugger.log("BluetoothDevice found :" + device);
bluetoothDevice = device;
int connectionState = headset.getConnectionState(bluetoothDevice);
Debugger.log("BluetoothHeadset connectionState "+connectionState);//2 == OK
boolean startVoiceRecognition = headset
.startVoiceRecognition(device);
if (startVoiceRecognition) {
Debugger
.log("BluetoothHeadset init Listener OK");
return;
}
else
Notify.popup("Bluetooth headset can't start speech recognition");
}
} catch (Exception e) {
// }
}
}
}
`
Monitoring of the BT status can be done indeed by polling. Here's how I did it (full sample here) . Note that it's just a sample and you should manage the polling better:
manifest
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
gradle
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.work:work-runtime-ktx:2.7.1"
MainActivityViewModel.kt
#UiThread
class MainActivityViewModel(application: Application) : BaseViewModel(application) {
private val bluetoothAdapter: BluetoothAdapter =
context.getSystemService<BluetoothManager>()!!.adapter
private var bluetoothHeadsetProfile: BluetoothProfile? = null
val connectedDevicesLiveData =
DistinctLiveDataWrapper(MutableLiveData<ConnectedDevicesState>(ConnectedDevicesState.Idle))
val bluetoothTurnedOnLiveData = DistinctLiveDataWrapper(MutableLiveData<Boolean?>(null))
val isConnectedToBtHeadsetLiveData = DistinctLiveDataWrapper(MutableLiveData<Boolean?>(null))
private val pollingBtStateRunnable: Runnable
init {
updateBtStates()
pollingBtStateRunnable = object : Runnable {
override fun run() {
updateBtStates()
handler.postDelayed(this, POLLING_TIME_IN_MS)
}
}
// Establish connection to the proxy.
val serviceListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, bluetoothProfile: BluetoothProfile) {
this#MainActivityViewModel.bluetoothHeadsetProfile = bluetoothProfile
handler.removeCallbacks(pollingBtStateRunnable)
pollingBtStateRunnable.run()
}
override fun onServiceDisconnected(profile: Int) {
handler.removeCallbacks(pollingBtStateRunnable)
updateBtStates()
}
}
bluetoothAdapter.getProfileProxy(context, serviceListener, BluetoothProfile.HEADSET)
onClearedListeners.add {
this.bluetoothHeadsetProfile?.let { bluetoothProfile ->
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothProfile)
}
handler.removeCallbacks(pollingBtStateRunnable)
}
}
fun initWithLifecycle(lifecycle: Lifecycle) {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
pollingBtStateRunnable.run()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
handler.removeCallbacks(pollingBtStateRunnable)
}
})
}
#UiThread
private fun updateBtStates() {
// Log.d("AppLog", "updateBtStates")
val isBlueToothTurnedOn = bluetoothAdapter.state == BluetoothAdapter.STATE_ON
bluetoothTurnedOnLiveData.value = isBlueToothTurnedOn
if (!isBlueToothTurnedOn) {
connectedDevicesLiveData.value = ConnectedDevicesState.BluetoothIsTurnedOff
isConnectedToBtHeadsetLiveData.value = false
return
}
val isConnectedToBtHeadset = try {
bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothAdapter.STATE_CONNECTED
} catch (e: SecurityException) {
null
}
isConnectedToBtHeadsetLiveData.value = isConnectedToBtHeadset
val bluetoothProfile = bluetoothHeadsetProfile
if (bluetoothProfile != null) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
val connectedDevicesSet = bluetoothProfile.connectedDevices.toHashSet()
val previousConnectedDevices =
(connectedDevicesLiveData.value as? ConnectedDevicesState.GotResult)?.connectedDevices
if (previousConnectedDevices == null || previousConnectedDevices != connectedDevicesSet)
connectedDevicesLiveData.value =
ConnectedDevicesState.GotResult(connectedDevicesSet)
} else {
connectedDevicesLiveData.value =
ConnectedDevicesState.NeedBlueToothConnectPermission
}
} else {
connectedDevicesLiveData.value = ConnectedDevicesState.Idle
}
}
companion object {
private const val POLLING_TIME_IN_MS = 500L
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
viewModel.initWithLifecycle(lifecycle)
viewModel.bluetoothTurnedOnLiveData.observe(this) {
Log.d("AppLog", "MainActivity bluetoothTurnedOnLiveData BT turned on? $it")
}
viewModel.isConnectedToBtHeadsetLiveData.observe(this) {
Log.d("AppLog", "MainActivity isConnectedToBtHeadsetLiveData BT headset connected? $it")
}
viewModel.connectedDevicesLiveData.observe(this) {
Log.d("AppLog", "MainActivity connectedDevicesLiveData devices: $it")
}
findViewById<View>(R.id.grantBtPermission).setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1)
}
}
}
}
ConnectedDevicesState.kt
sealed class ConnectedDevicesState {
object Idle : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "Idle"
return super.toString()
}
}
class GotResult(#Suppress("MemberVisibilityCanBePrivate") val connectedDevices: Set<BluetoothDevice>) : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG) {
return "GotResult: connectedDevices:${
connectedDevices.map {
try {
it.name
} catch (e: SecurityException) {
it.address
}
}
}"
}
return super.toString()
}
}
object BluetoothIsTurnedOff : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "BluetoothIsTurnedOff"
return super.toString()
}
}
object NeedBlueToothConnectPermission : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "NeedBlueToothConnectPermission"
return super.toString()
}
}
}
DistinctLiveDataWrapper.kt
class DistinctLiveDataWrapper<T>(#Suppress("MemberVisibilityCanBePrivate") val mutableLiveData: MutableLiveData<T>) {
#Suppress("MemberVisibilityCanBePrivate")
val distinctLiveData = Transformations.distinctUntilChanged(mutableLiveData)
var value: T?
#UiThread
set(value) {
mutableLiveData.value = value
}
get() {
return mutableLiveData.value
}
#AnyThread
fun postValue(value: T) {
mutableLiveData.postValue(value)
}
fun observe(lifecycleOwner: LifecycleOwner, observer: Observer<in T>) {
distinctLiveData.observe(lifecycleOwner, observer)
}
}
BaseViewModel.kt
/**usage: class MyViewModel(application: Application) : BaseViewModel(application)
* getting instance: private lateinit var viewModel: MyViewModel
* viewModel=ViewModelProvider(this).get(MyViewModel::class.java)*/
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
#Suppress("MemberVisibilityCanBePrivate")
var isCleared = false
#Suppress("MemberVisibilityCanBePrivate")
val onClearedListeners = ArrayList<Runnable>()
#Suppress("unused")
#SuppressLint("StaticFieldLeak")
val context: Context = application.applicationContext
#Suppress("unused")
val handler = Handler(Looper.getMainLooper())
override fun onCleared() {
super.onCleared()
isCleared = true
onClearedListeners.forEach { it.run() }
}
}

Categories

Resources