ConnectivityManager.NetworkCallback. onLost called with delay - android

I have NetworkUtils to monitor the connection state:
object NetworkUtils {
lateinit var connectivityManager: ConnectivityManager
var isConnected = false; private set
private object NetworkCallback : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
isConnected = true
}
override fun onLost(network: Network) {
isConnected = false
}
}
fun init(context: Context) {
connectivityManager = context.getSystemService(ConnectivityManager::class.java)
}
fun isConnectedDeprecated(): Boolean {
val networkInfo = connectivityManager.activeNetworkInfo
return networkInfo?.isConnected == true
}
fun registerNetworkCallback() = connectivityManager.registerDefaultNetworkCallback(NetworkCallback)
fun unregisterNetworkCallback() = connectivityManager.unregisterNetworkCallback(NetworkCallback)
}
And Interceptor I use with Retrofit:
class MyInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: IOException) {
throw if (NetworkUtils.isConnected()) {
ExceptionA()
} else {
ExceptionB()
}
}
}
}
The point is to know if IOException thrown from request caused by no connection (ExceptionB) or if it's some other network issue (ExceptionA).
The issue is if I turn off WIFI on my device in the middle of the request I expect to get ExceptionB, but sometimes I get ExceptionA. Because when interceptor catches IOException NetworkCallback's onLost isn't called yet.
I suspect that's because By default, the callback methods are called on the connectivity thread of your app, which is a separate thread used by ConnectivityManager. (link)
And Retorfit runs interceptors on a different thread. So there's no any guaranteed order.
So is there a way to be sure that NetworkCallback will be hit before interceptor will catch the exception?
I know we can pass in Handler when registering the NetworkCallback, and maybe that could help us to somehow run NetworkCallback on the same thread as Retrofit interceptors. But I have no idea how to do it and it looks like a bit dirty solution.
Also, if check NetworkUtils.isConnectedDeprecated() in interceptor instead of NetworkUtils.isConnected then it works exactly like I want to. But documentation says:
Deprecated. Apps should instead use the ConnectivityManager.NetworkCallback API to learn about connectivity changes. These will give a more accurate picture of the connectivity state of the device and let apps react more easily and quickly to changes.
So it's not more quickly if NetworkCallback is called with some delay, huh?

Related

Android Kotlin refresh textview

i want to update the local ip of the android system every time it changes in a textview, this is my code.
The function to obtain the ip is this
fun getIpv4HostAddress(): String {
NetworkInterface.getNetworkInterfaces()?.toList()?.map { networkInterface ->
networkInterface.inetAddresses?.toList()?.find {
!it.isLoopbackAddress && it is Inet4Address
}?.let { return it.hostAddress }
}
return ""
}
and the code inside the onCreate of the MainActivity.tk is this
val textView: TextView = findViewById(R.id.getIP)
textView.setText("IP local: " + getIpv4HostAddress())
textView.invalidate()
I want it to update and show it in real time in the texview, for example after setting and removing airplane mode, or changing networks wifi-> mobile mobile-> wifi
here I leave as seen in the application, someone to help me please
I've happened to have almost ready to use solution for this problem except extracting IPv4 address so I'll post it here so you could make use of it.
Basically, the solution consists of two main components: a "service" that listens to network changes and an RX subject to which you subscribe and post updates about network changes.
Step 0: Preparation
Make sure your AndroidManifest.xml file has next permissions included:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Your app has to enable compatibility options to allow the use of Java 8 features. Add the next lines in your build.gradle file:
android {
...
compileOptions {
targetCompatibility = "8"
sourceCompatibility = "8"
}
}
In order to make use of RX Kotlin add next dependencies:
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.0'
Step 1: Implement network change listener service
Imports are omitted to make code as concise as possible. NetworkReachabilityService is not a conventional Android service that you can start and it will run even when then the app is killed. It is a class that sets a listener to ConnectivityManager and handles all updates related to the network state.
Any type of update is handled similarly: something changed -> post NetworkState object with an appropriate value. On every change, we can request IPv4 to display in the UI (see on step 3).
sealed class NetworkState {
data class Available(val type: NetworkType) : NetworkState()
object Unavailable : NetworkState()
object Connecting : NetworkState()
object Losing : NetworkState()
object Lost : NetworkState()
}
sealed class NetworkType {
object WiFi : NetworkType()
object CELL : NetworkType()
object OTHER : NetworkType()
}
class NetworkReachabilityService private constructor(context: Application) {
private val connectivityManager: ConnectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
// There are more functions to override!
override fun onLost(network: Network) {
super.onLost(network)
postUpdate(NetworkState.Lost)
}
override fun onUnavailable() {
super.onUnavailable()
postUpdate(NetworkState.Unavailable)
}
override fun onLosing(network: Network, maxMsToLive: Int) {
super.onLosing(network, maxMsToLive)
postUpdate(NetworkState.Losing)
}
override fun onAvailable(network: Network) {
super.onAvailable(network)
updateAvailability(connectivityManager.getNetworkCapabilities(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
super.onCapabilitiesChanged(network, networkCapabilities)
updateAvailability(networkCapabilities)
}
}
companion object {
// Subscribe to this subject to get updates on network changes
val NETWORK_REACHABILITY: BehaviorSubject<NetworkState> =
BehaviorSubject.createDefault(NetworkState.Unavailable)
private var INSTANCE: NetworkReachabilityService? = null
#RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
fun getService(context: Application): NetworkReachabilityService {
if (INSTANCE == null) {
INSTANCE = NetworkReachabilityService(context)
}
return INSTANCE!!
}
}
private fun updateAvailability(networkCapabilities: NetworkCapabilities?) {
if (networkCapabilities == null) {
postUpdate(NetworkState.Unavailable)
return
}
var networkType: NetworkType = NetworkType.OTHER
if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
networkType = NetworkType.CELL
}
if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
networkType = NetworkType.WiFi
}
postUpdate(NetworkState.Available(networkType))
}
private fun postUpdate(networkState: NetworkState) {
NETWORK_REACHABILITY.onNext(networkState)
}
fun pauseListeningNetworkChanges() {
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (e: IllegalArgumentException) {
// Usually happens only once if: "NetworkCallback was not registered"
}
}
fun resumeListeningNetworkChanges() {
pauseListeningNetworkChanges()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.registerDefaultNetworkCallback(networkCallback)
} else {
connectivityManager.registerNetworkCallback(
NetworkRequest.Builder().build(),
networkCallback
)
}
}
}
Step 2: Implement a method to extract IPv4 (bonus IPv6)
I had to modify your IPv4 extraction a little as it did not return any IPv4 addresses while a device clearly had one. These are two methods to extract IPv4 and IPv6 addresses respectively. Methods were modified using this SO answer on how to extract IP addresses. Overall, it is 90% the same mapping of inetAddresses to the IP address values.
Add these two methods to NetworkReachabilityService class:
fun getIpv4HostAddress(): String? =
NetworkInterface.getNetworkInterfaces()?.toList()?.mapNotNull { networkInterface ->
networkInterface.inetAddresses?.toList()
?.filter { !it.isLoopbackAddress && it.hostAddress.indexOf(':') < 0 }
?.mapNotNull { if (it.hostAddress.isNullOrBlank()) null else it.hostAddress }
?.firstOrNull { it.isNotEmpty() }
}?.firstOrNull()
fun getIpv6HostAddress(): String? =
NetworkInterface.getNetworkInterfaces()?.toList()?.mapNotNull { networkInterface ->
networkInterface.inetAddresses?.toList()
?.filter { !it.isLoopbackAddress && it is Inet6Address }
?.mapNotNull { if (it.hostAddress.isNullOrBlank()) null else it.hostAddress }
?.firstOrNull { it.isNotEmpty() }
}?.firstOrNull()
Step 3: Update UI
The simples solution related to UI is a direct subscription to NETWORK_REACHABILITY subject and on each change received through that subject, we pull out IPv4 data from NetworkReachabilityService and display it in the UI. Two main methods you want to look at are subscribeToUpdates and updateIPv4Address. And do not forget to unsubscribe by using unsubscribeFromUpdates to prevent memory leaks.
class MainActivity : AppCompatActivity() {
private val compositeDisposable = CompositeDisposable()
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.text_view)
val service = NetworkReachabilityService.getService(application)
service.resumeListeningNetworkChanges()
subscribeToUpdates()
}
override fun onDestroy() {
super.onDestroy()
unsubscribeFromUpdates()
}
private fun unsubscribeFromUpdates() {
compositeDisposable.dispose()
compositeDisposable.clear()
}
private fun subscribeToUpdates() {
val disposableSubscription =
NetworkReachabilityService.NETWORK_REACHABILITY
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ networkState ->
// We do not care about networkState right now
updateIPv4Address()
}, {
// Handle the error
it.printStackTrace()
})
compositeDisposable.addAll(disposableSubscription)
}
private fun updateIPv4Address() {
val service = NetworkReachabilityService.getService(application)
textView.text = service.getIpv4HostAddress()
}
}
Recap
Using a ConnectivityManager instance we set a listener which reacts on any network change. Each change triggers an update which posts value to RX subject holding the latest network state. By subscribing to the subject we can track network state changes and assume the device had its address changed and thus we refresh IPv4 value displayed in a TextView.
I decided this code was good to go on GitHub, so here is the link to the project.
To receive event information at real time you can use different ways depending upon if your app is in foreground or background when the info is needed.
Since in your case the app seems to be in foreground, you make use of application.class to write code for receiving network changes using broadcast receiver( programatically registered) or some other way. And then in the function that receives that event change info , make a call to your getIpv4HostAddress() that would set the ip string and use it in the set the textview in another calss.

Combine WiFi Specifier and Suggestion APIs on Android Q

I am building an app where the connection to a 2nd device is the essence. Therefore, I used the WifiNetworkSpecifier API. However, the application must be able to automatically reconnect to the target network once the users leave and return to the Wi-Fi perimeter. Thus, I used the WifiNetworkSuggestion API. However, I am experiencing several issues there:
Once I get connected to the SSID using the specifier API and I confirm the push notification generated by the suggestion API, the suggestion API does not seem to work until I manually disconnect from the SSID (unregister network callback previously assigned to the specifier request) or kill the application.
If there is another network present in the perimeter which the user previously connected to by using the OS Wi-Fi manager (a hotspot, for instance), Android will prioritize this network, hence the suggestion API for my application would never auto-reconnect to the wanted and accessible SSID.
From my experience and understanding (which might be wrong) so far, it seems like we have to manually unregister the network callback previously assigned to the specifier request, or kill the application, and let the suggestion API to do its thing until it can work properly. This might be problematic if there are other networks (which the user previously connected to by using the OS Wi-Fi manager) present in the perimeter. In this case, we'd never auto-reconnect to the SSID defined by the application and the suggestion API would never work.
The question is: how to combine those two APIs to be able to connect to an SSID, yet auto-reconnect, without doing such ugly hacks as manually disconnecting the user, or killing the application, which also doesn't give us any guarantees?
In my opinion, this whole new implementation with the new network APIs is not done well, it's creating a lot of issues and restrictions for developers, or at least it's poorly documented.
Here's the code used for making the requests. Note that the device I'm connecting to does not have actual internet access, it's just used as a p2p network.
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun connectToWiFiOnQ(wifiCredentials: WifiCredentials, onUnavailable: () -> Unit) {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(createWifiNetworkSpecifier(wifiCredentials))
.build()
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
connectivityManager.bindProcessToNetwork(network)
}
override fun onUnavailable() {
super.onUnavailable()
onUnavailable.invoke()
}
}
networkCallback?.let {
addNetworkSuggestion(wifiCredentials)
connectivityManager.requestNetwork(request, it)
}
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun addNetworkSuggestion(wifiCredentials: WifiCredentials) {
wifiManager.addNetworkSuggestions(listOf(createWifiNetworkSuggestion(wifiCredentials))).apply {
if (this != WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
if (this == WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP) {
wifiManager.removeNetworkSuggestions(emptyList())
addNetworkSuggestion(wifiCredentials)
}
}
}
suggestionBroadcastReceiver?.let { context.unregisterReceiver(it) }
suggestionBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)
return
// Post connection processing..
}
}
context.registerReceiver(
suggestionBroadcastReceiver, IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)
)
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun createWifiNetworkSpecifier(wifiCredentials: WifiCredentials): WifiNetworkSpecifier {
return when (wifiCredentials.authenticationType.toLowerCase()) {
WifiCipherType.NOPASS.name.toLowerCase() -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
WifiCipherType.WPA.name.toLowerCase() -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setWpa2Passphrase(wifiCredentials.password)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
else -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
}
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun createWifiNetworkSuggestion(wifiCredentials: WifiCredentials): WifiNetworkSuggestion {
return when (wifiCredentials.authenticationType.toLowerCase()) {
WifiCipherType.NOPASS.name.toLowerCase() -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
WifiCipherType.WPA.name.toLowerCase() -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setWpa2Passphrase(wifiCredentials.password)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
else -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
}
}
Calling the suggestion API in onAvailable works for me. That way the user doesn't see two popups at the same time either.
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
connectivityManager.bindProcessToNetwork(network)
addNetworkSuggestion(wifiCredentials)
}
}

Suspending function test with MockWebServer

I'm testing api that returns result using suspending function with MockWebServer, but it does not work with runBlockingTest, testCoroutineDispatcher, testCorounieScope unless a launch builder is used, why?
abstract class AbstractPostApiTest {
internal lateinit var mockWebServer: MockWebServer
private val responseAsString by lazy {
getResourceAsText(RESPONSE_JSON_PATH)
}
#BeforeEach
open fun setUp() {
mockWebServer = MockWebServer()
println("AbstractPostApiTest setUp() $mockWebServer")
}
#AfterEach
open fun tearDown() {
mockWebServer.shutdown()
}
companion object {
const val RESPONSE_JSON_PATH = "posts.json"
}
#Throws(IOException::class)
fun enqueueResponse(
code: Int = 200,
headers: Map<String, String>? = null
): MockResponse {
// Define mock response
val mockResponse = MockResponse()
// Set response code
mockResponse.setResponseCode(code)
// Set headers
headers?.let {
for ((key, value) in it) {
mockResponse.addHeader(key, value)
}
}
// Set body
mockWebServer.enqueue(
mockResponse.setBody(responseAsString)
)
return mockResponse
}
}
class PostApiTest : AbstractPostApiTest() {
private lateinit var postApi: PostApiCoroutines
private val testCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
#BeforeEach
override fun setUp() {
super.setUp()
val okHttpClient = OkHttpClient
.Builder()
.build()
postApi = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(PostApiCoroutines::class.java)
Dispatchers.setMain(testCoroutineDispatcher)
}
#AfterEach
override fun tearDown() {
super.tearDown()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
#Test
fun `Given we have a valid request, should be done to correct url`() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
postApi.getPostsResponse()
advanceUntilIdle()
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
}
Results error: java.lang.IllegalStateException: This job has not completed yet
This test does not work if launch builder is used, and if launch builder is used it does not require testCoroutineDispatcher or testCoroutineScope, what's the reason for this? Normally suspending functions pass without being in another scope even with runBlockingTest
#Test
fun `Given we have a valid request, should be done to correct url`() =
runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
launch {
postApi.getPosts()
}
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
The one above works.
Also the test below pass some of the time.
#Test
fun Given api return 200, should have list of posts() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200)
// WHEN
var posts: List<Post> = emptyList()
launch {
posts = postApi.getPosts()
}
advanceUntilIdle()
// THEN
Truth.assertThat(posts).isNotNull()
Truth.assertThat(posts.size).isEqualTo(100)
}
I tried many combinations invoking posts = postApi.getPosts() without launch, using async, putting enqueueResponse(200) inside async async { enqueueResponse(200) }.await() but tests failed, sometimes it pass sometimes it does not some with each combination.
There is a bug with runBlockTest not waiting for other threads/jobs to complete before completing the coroutine that the test is running in.
I tried using runBlocking with success (I use the awesome port of Hamcrest to Kotlin Hamkrest)
fun `run test` = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(""))
// make HTTP call
val result = mockWebServer.takeRequest(2000L, TimeUnit.MILLISECONDS)
assertThat(result != null, equalTo(true))
}
There's a few things to note here:
The use of thread blocking calls should never be called without a timeout. Always better to fail with nothing, then to block a thread forever.
The use of runBlocking might be considered by some to be no no. However this blog post outlines the different method of running concurrent code, and the different use cases for them. We normally want to use runBlockingTest or (TestCoroutineDispatcher.runBlockingTest) so that our test code and app code are synchronised. By using the same Dispatcher we can make sure that the jobs all finish, etc. TestCoroutineDispatcher also has that handy "clock" feature to make delays disappear. However when testing the HTTP layer of the application, and where there is a mock server running on a separate thread we have a synchronisation point being takeRequest. So we can happily use runBlocking to allow us to use coroutines and a mock server running on a different thread work together with no problems.

How make a SMB connection through Network object in Android?

I'm trying to make a SMB (Samba) connection to get a list of files and download them with the SMBClient of smbj library.
To that I have to connect to a specific network and use that class, but in Android Q I have to change the way to connect to the wireless, like this:
val wifiNetworkSpecifier: WifiNetworkSpecifier = WifiNetworkSpecifier.Builder().apply {
setSsid(ssid)
setWpa2Passphrase(password)
}.build()
val networkRequest: NetworkRequest = NetworkRequest.Builder().apply {
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
setNetworkSpecifier(wifiNetworkSpecifier)
}.build()
val networkCallback: ConnectivityManager.NetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.d(tag, "::onAvailable - Entry")
super.onAvailable(network)
}
override fun onUnavailable() {
Log.d(tag, "::onUnavailable - Entry")
super.onUnavailable()
}
}
This makes a connection in the app, but establishes the main connection via mobile data and I can't establish a connection because the server is unreachable. I have to find a way to make the connection through the network object in the onAvailable function.
Did you know how or is there an alternative way?
Solution
I found a method in the ConnectivityManager class the method is bindProcessToNetwork
connectivityManager.bindProcessToNetwork(network)
I found a method in the ConnectivityManager class the method is bindProcessToNetwork
connectivityManager.bindProcessToNetwork(network)

Kotlin callback not called within async function

I am attempting to subscribe to multiple characteristics of a BLE peripheral within Android API 28.
Due to the asynchronous nature of the BLE API I need to make the function that subscribes to each characteristic (gatt.writeDescriptor()) block; otherwise the BLE API will attempt to subscribe to multiple characteristics at once, despite the fact that only one descriptor can be written at a time: meaning that only one characteristic is ever subscribed to.
The blocking is achieved by overriding the onServicesDiscovered callback and calling an asynchronous function to loop through and subscribe to characteristics. This is blocked with a simple Boolean value (canContinue). Unfortunately, the callback function onDescriptorWrite is never called.
See the code below:
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
canContinue = true
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
runBlocking {
loopAsync(gatt)
}
}
private suspend fun loopAsync(gatt: BluetoothGatt) {
coroutineScope {
async {
gatt.services.forEach { gattService ->
gattService.characteristics.forEach { gattChar ->
CHAR_LIST.forEach {
if (gattChar.uuid.toString().contains(it)) {
canContinue = false
gatt.setCharacteristicNotification(gattChar, true)
val descriptor = gattChar.getDescriptor(UUID.fromString(BleNamesResolver.CLIENT_CHARACTERISTIC_CONFIG))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
val write = Runnable {
gatt.writeDescriptor(descriptor)
}
//private val mainHandler = Handler(Looper.getMainLooper())
//mainHandler.post(write)
//runOnUiThread(write)
gatt.writeDescriptor(descriptor)
}
while (!canContinue)
}
}
}
}
}
}
It was suggested in a related post that I run the gatt.writeDescriptor() function in the main thread. As you can see in the code above I have tried this to no avail using both runOnUiThread() and creating a Handler object following suggestions from this question.
The callback gets called if I call gatt.writeDescriptor() from a synchronous function, I have no idea why it doesn't get called from an asynchronous function.
EDIT: It appears that the while(!canContinue); loop is actually blocking the callback. If I comment this line out, the callback triggers but then I face the same issue as before. How can I block this function?
Any suggestions are most welcome! Forgive my ignorance, but I am very much used to working on embedded systems, Android is very much a new world to me!
Thanks,
Adam
I posted some notes in the comments but I figured it would be better to format it as an answer.
Even though you already fixed your issue I'd suggest running the actual coroutine asynchronously and inside of it wait for the write notification using channels
private var channel: Channel<Boolean> = Channel()
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
GlobalScope.async {
channel.send(true)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
GlobalScope.async {
loopAsync(gatt)
}
}
private suspend fun loopAsync(gatt: BluetoothGatt) {
gatt.services.forEach { gattService ->
gattService.characteristics.forEach { gattChar ->
CHAR_LIST.forEach {
if (gattChar.uuid.toString().contains(it)) {
gatt.setCharacteristicNotification(gattChar, true)
val descriptor = gattChar.getDescriptor(UUID.fromString(BleNamesResolver.CLIENT_CHARACTERISTIC_CONFIG))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
channel.receive()
}
}
}
}
}
So I actually figured out the answer myself.
The while(!canContinue); loop was actually blocking the callback as it was running in the main thread and took priority over the callback required to set the canContinue variable.
This was solved simply by calling both the gatt.writeDescriptor() function and the while loop from within the main thread:
val subscribe = Runnable {
gatt.writeDescriptor(descriptor)
while (!canContinue);
}
runOnUiThread(subscribe)

Categories

Resources