<commit_before>package io.particle.mesh.bluetooth.connecting

import androidx.lifecycle.LiveData
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.content.Context
import androidx.annotation.MainThread
import io.particle.mesh.bluetooth.BTCharacteristicWriter
import io.particle.mesh.bluetooth.BLELiveDataCallbacks
import io.particle.mesh.bluetooth.btAdapter
import io.particle.mesh.bluetooth.connecting.ConnectionState.DISCONNECTED
import io.particle.mesh.common.QATool
import io.particle.mesh.common.truthy
import io.particle.mesh.setup.connection.BT_SETUP_RX_CHARACTERISTIC_ID
import io.particle.mesh.setup.connection.BT_SETUP_SERVICE_ID
import io.particle.mesh.setup.connection.BT_SETUP_TX_CHARACTERISTIC_ID
import io.particle.mesh.setup.ui.utils.buildMatchingDeviceNameSuspender
import io.particle.mesh.setup.utils.checkIsThisTheMainThread
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.ReceiveChannel
import kotlinx.coroutines.experimental.channels.SendChannel
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withTimeoutOrNull
import mu.KotlinLogging


enum class ConnectionPriority(val sdkVal: Int) {
    HIGH(1),
    BALANCED(0),
    LOW_POWER(2)
}


class BluetoothConnection(
        val connectionStateChangedLD: LiveData<ConnectionState?>,
        private val gatt: BluetoothGatt,
        // this channel receives arbitrary-length arrays (not limited to BLE MTU)
        val packetSendChannel: SendChannel<ByteArray>,
        // this channel emits arbitrary-length arrays (not limited to BLE MTU)
        private val closablePacketReceiveChannel: Channel<ByteArray>
) {

    init {
        connectionStateChangedLD.observeForever {
            if (it == DISCONNECTED) {
                disconnect(false)
            }
        }
    }

    val deviceName: String
        get() = gatt.device.name

    val isConnected: Boolean
        get() = connectionStateChangedLD.value == ConnectionState.CONNECTED

    val packetReceiveChannel: ReceiveChannel<ByteArray>
        get() = closablePacketReceiveChannel

    fun setConnectionPriority(priority: ConnectionPriority) {
        gatt.requestConnectionPriority(priority.sdkVal)
    }

    fun disconnect(closeGatt: Boolean = true) {
        QATool.runSafely(
                { packetSendChannel.close() },
                { closablePacketReceiveChannel.close() },
                { gatt.disconnect() }
        )
        if (!closeGatt) {
            return
        }
        // calling .close() *immediately* after .disconnect() was sometimes causing
        // the disconnect to fail, thus the delay.  Hacky, but it works. :-/
        launch(UI) {
            delay(100)
            QATool.runSafely({ gatt.close() })
        }
    }
}


typealias BTDeviceAddress = String

const val CONNECTION_TIMEOUT_MILLIS = 10000


class BluetoothConnectionManager(private val ctx: Context) {

    private val log = KotlinLogging.logger {}


    @MainThread
    suspend fun connectToDevice(
            deviceName: String,
            timeout: Int = CONNECTION_TIMEOUT_MILLIS
    ): BluetoothConnection? {
        checkIsThisTheMainThread()

        val address = scanForDevice(deviceName, timeout) ?: return null

        log.info { "Connecting to device $address" }
        // 1. Attempt to connect
        val device = ctx.btAdapter.getRemoteDevice(address)
        // If this returns null, we're finished, return null ourselves
        val (gatt, bleWriteChannel, callbacks) = doConnectToDevice(device) ?: return null


        val messageWriteChannel = Channel<ByteArray>(Channel.UNLIMITED)
        launch {
            for (packet in messageWriteChannel) {
                QATool.runSafely({ bleWriteChannel.writeToCharacteristic(packet) })
            }
        }

        return BluetoothConnection(
                callbacks.connectionStateChangedLD,
                gatt,
                messageWriteChannel,
                callbacks.readOrChangedReceiveChannel as Channel<ByteArray>
        )
    }

    private suspend fun scanForDevice(deviceName: String, timeout: Int): BTDeviceAddress? {
        val scannerSuspender = buildMatchingDeviceNameSuspender(ctx, deviceName)
        val scanResult = withTimeoutOrNull(timeout) {
            scannerSuspender.awaitResult()
        }
        return scanResult?.device?.address
    }

    private suspend fun doConnectToDevice(
            device: BluetoothDevice
    ): Triple<BluetoothGatt, BTCharacteristicWriter, BLELiveDataCallbacks>? {
        val gattConnectionCreator = GattConnector(ctx)
        log.info { "Creating connection to device $device" }<commit_msg>Ensure OS isn't remembering priority state across connections<commit_after>package io.particle.mesh.bluetooth.connecting

import androidx.lifecycle.LiveData
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.content.Context
import androidx.annotation.MainThread
import io.particle.mesh.bluetooth.BTCharacteristicWriter
import io.particle.mesh.bluetooth.BLELiveDataCallbacks
import io.particle.mesh.bluetooth.btAdapter
import io.particle.mesh.bluetooth.connecting.ConnectionPriority.BALANCED
import io.particle.mesh.bluetooth.connecting.ConnectionState.DISCONNECTED
import io.particle.mesh.common.QATool
import io.particle.mesh.common.truthy
import io.particle.mesh.setup.connection.BT_SETUP_RX_CHARACTERISTIC_ID
import io.particle.mesh.setup.connection.BT_SETUP_SERVICE_ID
import io.particle.mesh.setup.connection.BT_SETUP_TX_CHARACTERISTIC_ID
import io.particle.mesh.setup.ui.utils.buildMatchingDeviceNameSuspender
import io.particle.mesh.setup.utils.checkIsThisTheMainThread
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.ReceiveChannel
import kotlinx.coroutines.experimental.channels.SendChannel
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withTimeoutOrNull
import mu.KotlinLogging


enum class ConnectionPriority(val sdkVal: Int) {
    HIGH(1),
    BALANCED(0),
    LOW_POWER(2)
}


class BluetoothConnection(
        val connectionStateChangedLD: LiveData<ConnectionState?>,
        private val gatt: BluetoothGatt,
        // this channel receives arbitrary-length arrays (not limited to BLE MTU)
        val packetSendChannel: SendChannel<ByteArray>,
        // this channel emits arbitrary-length arrays (not limited to BLE MTU)
        private val closablePacketReceiveChannel: Channel<ByteArray>
) {

    init {
        connectionStateChangedLD.observeForever {
            if (it == DISCONNECTED) {
                disconnect(false)
            }
        }
    }

    val deviceName: String
        get() = gatt.device.name

    val isConnected: Boolean
        get() = connectionStateChangedLD.value == ConnectionState.CONNECTED

    val packetReceiveChannel: ReceiveChannel<ByteArray>
        get() = closablePacketReceiveChannel

    fun setConnectionPriority(priority: ConnectionPriority) {
        gatt.requestConnectionPriority(priority.sdkVal)
    }

    fun disconnect(closeGatt: Boolean = true) {
        QATool.runSafely(
                { packetSendChannel.close() },
                { closablePacketReceiveChannel.close() },
                { gatt.disconnect() }
        )
        if (!closeGatt) {
            return
        }
        // calling .close() *immediately* after .disconnect() was sometimes causing
        // the disconnect to fail, thus the delay.  Hacky, but it works. :-/
        launch(UI) {
            delay(100)
            QATool.runSafely({ gatt.close() })
        }
    }
}


typealias BTDeviceAddress = String

const val CONNECTION_TIMEOUT_MILLIS = 10000


class BluetoothConnectionManager(private val ctx: Context) {

    private val log = KotlinLogging.logger {}


    @MainThread
    suspend fun connectToDevice(
            deviceName: String,
            timeout: Int = CONNECTION_TIMEOUT_MILLIS
    ): BluetoothConnection? {
        checkIsThisTheMainThread()

        val address = scanForDevice(deviceName, timeout) ?: return null

        log.info { "Connecting to device $address" }
        // 1. Attempt to connect
        val device = ctx.btAdapter.getRemoteDevice(address)
        // If this returns null, we're finished, return null ourselves
        val (gatt, bleWriteChannel, callbacks) = doConnectToDevice(device) ?: return null


        val messageWriteChannel = Channel<ByteArray>(Channel.UNLIMITED)
        launch {
            for (packet in messageWriteChannel) {
                QATool.runSafely({ bleWriteChannel.writeToCharacteristic(packet) })
            }
        }

        val conn = BluetoothConnection(
                callbacks.connectionStateChangedLD,
                gatt,
                messageWriteChannel,
                callbacks.readOrChangedReceiveChannel as Channel<ByteArray>
        )
        // this is the default, but ensure that the OS isn't remembering it from the
        // previous connection to the device
        conn.setConnectionPriority(ConnectionPriority.BALANCED)
        return conn
    }

    private suspend fun scanForDevice(deviceName: String, timeout: Int): BTDeviceAddress? {
        val scannerSuspender = buildMatchingDeviceNameSuspender(ctx, deviceName)
        val scanResult = withTimeoutOrNull(timeout) {
            scannerSuspender.awaitResult()
        }
        return scanResult?.device?.address
    }

    private suspend fun doConnectToDevice(
            device: BluetoothDevice
    ): Triple<BluetoothGatt, BTCharacteristicWriter, BLELiveDataCallbacks>? {
        val gattConnectionCreator = GattConnector(ctx)
        log.info { "Creating connection to device $device" }
{'fraction_non_alphanumeric': 0.0759144237405107, 'fraction_numerical': 0.0023661638568470865, 'mean_word_length': 3.1573770491803277, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5455657', 'n_tokens_mistral': 2883, 'n_tokens_neox': 2738, 'n_words': 578}