polarofficial / polar-ble-sdk

Repository includes SDK and code examples. More info https://polar.com/en/developers
Other
475 stars 153 forks source link

Unable to reconnect on foreground service #314

Closed hfsamb closed 1 year ago

hfsamb commented 1 year ago

Platform your question concerns:

Device:

Description: I made a foreground service that receives heartbeat values from polar and I noticed that the reconnect feature is not working if the app does not have an activity on screen.

In a small test app shown below there is an activity that will get polar id from user and a button that will start foreground service. In the service I connect to device using polar ble api and can get periodic HB callbacks but if connection is lost then it is not reconnected automatically.

I noticed that if I keep the activity that started the service on display then I can reconnect as expected. If activity is paused or finished then the service cannot reconnect.

Is there a limitation on reconnect feature if sdk connection was made in service?

In the tests attached logs, device lost connection due to power down and reconnection was performed with a power up after a few seconds.


class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    companion object {
        const val PERMISSION_REQUEST_CODE = 9876321
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT), PERMISSION_REQUEST_CODE)
            } else {
                requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_CODE)
            }
        } else {
            requestPermissions(arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), PERMISSION_REQUEST_CODE)
        }

        binding.button.setOnClickListener {
            val intent = Intent(applicationContext, HeartbeatService::class.java)
            intent.putExtra(HeartbeatService.POLAR_ID, binding.editTextPolarId.text.toString())
            startService(intent)

//             startActivity( Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME))
            // or
            // finish()
            // make reconnection not work
        }

        setContentView(binding.root)
    }
}

class HeartbeatService : Service() {

    private lateinit var polarBleApi: PolarBleApi

    private lateinit var polarId: String

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    companion object {
        private const val SERVICE_ID = 875642
        const val POLAR_ID = "key.polar.id"
        private const val CHANNEL_ID = "channel_id"
        private const val CHANNEL_NAME = "channel_name"
        private const val TAG = "HeartbeatService"
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        startForeground(SERVICE_ID, serviceNotification())

        polarBleApi = PolarBleApiDefaultImpl.defaultImplementation(
            applicationContext, PolarBleApi.ALL_FEATURES)

        if (intent != null) {
            polarId = intent.getStringExtra(POLAR_ID).toString()
        }
        connectPolar(polarId)

        return super.onStartCommand(intent, flags, startId)
    }

    private fun serviceNotification(): Notification {
        val notificationManager = getSystemService(NotificationManager::class.java)
        notificationManager.createNotificationChannel(
            NotificationChannel(
                CHANNEL_ID,
                CHANNEL_NAME,
                NotificationManager.IMPORTANCE_DEFAULT
            )
        )
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentText("Heartbeat service in execution")
            .build()
    }

    private fun connectPolar(polarId: String) {
        polarBleApi.run {
            setApiLogger { s: String? -> Log.d(TAG, "POLAR BLE API LOGGER $s") }
            connectToDevice(polarId)
            setAutomaticReconnection(true)
            setPolarCallback()
        }
    }

    private fun setPolarCallback() {
        polarBleApi.setApiCallback(object : PolarBleApiCallback() {
            override fun blePowerStateChanged(powered: Boolean) {
                Log.d(TAG, "BLE power: $powered")
            }

            override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) {
                Log.d(TAG, "CONNECTED: " + polarDeviceInfo.deviceId)
            }

            override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) {
                Log.d(TAG, "CONNECTING: " + polarDeviceInfo.deviceId)
            }

            override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) {
                Log.d(TAG, "DISCONNECTED: " + polarDeviceInfo.deviceId)
            }

            override fun streamingFeaturesReady(
                identifier: String,
                features: Set<PolarBleApi.DeviceStreamingFeature?>
            ) {
                for (feature in features) {
                    Log.d(
                        TAG,
                        "Streaming feature " + feature.toString().toString() + " is ready"
                    )
                }
            }

            override fun hrFeatureReady(identifier: String) {
                Log.d(TAG, "HR READY: $identifier")
            }

            override fun disInformationReceived(identifier: String, uuid: UUID, value: String) {}
            override fun batteryLevelReceived(identifier: String, level: Int) {}
            override fun hrNotificationReceived(identifier: String, data: PolarHrData) {
                Log.d(TAG, "HR: " + data.hr)
            }

            override fun polarFtpFeatureReady(s: String) {}
        })
    }
}

fail.txt pass.txt

JOikarinen commented 1 year ago

Hi @hfsamb, thanks for the good description and sample code. I may have an idea what is the problem and it is most probably related to the Android Bluetooth permissions.

Android 12 onwards there are dedicated permissions for Bluetooth. Android 11 and below requires the location permission to be approved by the app user to make BLE scanning to work.

I believe the permissions are correctly asked in your application, but in addition there is a small detail related to Android service. As the Android 11 and below needs the location permission for Bluetooth scanning, also the service needs to be declare with android:foregroundServiceType="location"

By using your code snippet I did small example project https://github.com/JOikarinen/PolarBleSdkWithAndroidService which shall work as expected. Please have a try.

hfsamb commented 1 year ago

Hi @JOikarinen , thank you for the answer, it solved the problem indeed.

Should I add the ACCESS_BACKGROUND_LOCATION permission to the app? It seems to be required according to docs but it worked without it in my tests.

Should I add the connectedDevice foreground service type, as in android:foregroundServiceType="connectedDevice|location" ? Is it required for long timed connection to Polar?

JOikarinen commented 1 year ago

Hi @hfsamb,

Should I add the ACCESS_BACKGROUND_LOCATION permission to the app? It seems to be required according to docs but it worked without it in my tests.

Should I add the connectedDevice foreground service type, as in android:foregroundServiceType="connectedDevice|location" ? Is it required for long timed connection to Polar?

  • I must admit, I couldn't really found out the exact description for "connectedDevice". I am not sure is it needed. The documentation don't tell me exactly when it is needed:
    Auto, bluetooth, TV or other devices connection, monitoring and interaction.
  • the key word "bluetooth" is mentioned in documentation, so maybe it shall be added as argument too. In my own testing I have only used android:foregroundServiceType="location" without problems.