Android Smart TV Device Not Showing Pairing Code
16:54 02 Jan 2026

I am working on Android TV Remote App. I am using Connect SDK to discover devices. To pair android app with smart TV I am using Direct SSL connection to TV's IP address on port 6467 and for pairing I am sending protobuf messages (Pairing Request → Options → Configuration) and expected TV shows 6-digit code on screen for pairing but It is not showing at all, Here is my code snippet :

class AndroidTVPairingManager(
        private val deviceIp: String,
        private val deviceName: String,
        private val listener: AndroidTVPairingListener
    ) {
        private var sslSocket: SSLSocket? = null
        private var inputStream: InputStream? = null
        private var outputStream: OutputStream? = null
        private var isConnected = false
        private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
        private val protocol = AndroidTVRemoteProtocol()
    
        private val PAIRING_PORT = 6467
        private val CONNECTION_TIMEOUT = 15000
    
        companion object {
            private const val TAG = "AndroidTVPairing"
    
            init {
                Security.removeProvider("BC")
                Security.addProvider(BouncyCastleProvider())
            }
        }
    
        interface AndroidTVPairingListener {
            fun onPairingCodeRequired(code: String)
            fun onPairingSuccess()
            fun onPairingFailed(error: String)
            fun onConnectionEstablished()
            fun onDisconnected()
        }
    
        fun startPairing() {
            scope.launch {
                try {
                    Log.d(TAG, "=== Starting Pairing ===")
    
                    if (!establishSSLConnection()) {
                        withContext(Dispatchers.Main) {
                            listener.onPairingFailed("SSL connection failed")
                        }
                        return@launch
                    }
    
                    // CORRECT SEQUENCE from reference app:
                    // 1. Send pairing request
                    // 2. Send OPTIONS (we send, not receive!)
                    // 3. Send CONFIGURATION
                    // 4. Wait for SECRET from TV
    
                    sendPairingRequest()
                    sendOptions()
                    sendConfiguration()
    
                    // NOW flush all at once
                    outputStream?.flush()
                    Log.d(TAG, "✓ All messages flushed")
    
                    // TV should show pairing dialog now
                    Log.d(TAG, "✓✓✓ CHECK TV SCREEN FOR CODE ✓✓✓")
    
                    withContext(Dispatchers.Main) {
                        listener.onPairingCodeRequired("")
                    }
    
                } catch (e: Exception) {
                    Log.e(TAG, "❌ Error: ${e.message}", e)
                    withContext(Dispatchers.Main) {
                        listener.onPairingFailed("Error: ${e.message}")
                    }
                    cleanup()
                }
            }
        }
    
        private suspend fun establishSSLConnection(): Boolean {
            return withContext(Dispatchers.IO) {
                try {
                    val baseSocket = Socket()
                    baseSocket.connect(InetSocketAddress(deviceIp, PAIRING_PORT), CONNECTION_TIMEOUT)
                    baseSocket.tcpNoDelay = true
    
                    val sslContext = createSSLContext()
                    sslSocket = sslContext.socketFactory.createSocket(
                        baseSocket, deviceIp, PAIRING_PORT, true
                    ) as SSLSocket
    
                    sslSocket?.apply {
                        useClientMode = true
                        enabledProtocols = supportedProtocols
                        startHandshake()
                    }
    
                    Log.d(TAG, "✓ SSL connected")
    
                    inputStream = BufferedInputStream(sslSocket?.inputStream)
                    outputStream = BufferedOutputStream(sslSocket?.outputStream)
                    true
    
                } catch (e: Exception) {
                    Log.e(TAG, "❌ SSL failed!", e)
                    false
                }
            }
        }
    
        private fun createSSLContext(): SSLContext {
            val keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC")
            keyPairGenerator.initialize(2048, SecureRandom())
            val keyPair = keyPairGenerator.generateKeyPair()
            val certificate = generateCertificate(keyPair)
    
            val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
            keyStore.load(null, null)
            keyStore.setKeyEntry("client", keyPair.private, "password".toCharArray(), arrayOf(certificate))
    
            val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
            keyManagerFactory.init(keyStore, "password".toCharArray())
    
            val trustAllCerts = arrayOf(object : X509TrustManager {
                override fun checkClientTrusted(chain: Array, authType: String) {}
                override fun checkServerTrusted(chain: Array, authType: String) {}
                override fun getAcceptedIssuers(): Array = arrayOf()
            })
    
            val sslContext = SSLContext.getInstance("TLS")
            sslContext.init(keyManagerFactory.keyManagers, trustAllCerts, SecureRandom())
            return sslContext
        }
    
        private fun generateCertificate(keyPair: KeyPair): X509Certificate {
            val now = Date()
            val notBefore = Date(now.time - 1000L * 60 * 60 * 24)
            val notAfter = Date(now.time + 1000L * 60 * 60 * 24 * 365 * 10)
            val serialNumber = BigInteger.valueOf(now.time)
            val subject = X500Name("CN=androidtv/livingTV")
            val publicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.public.encoded)
    
            val certBuilder = X509v3CertificateBuilder(
                subject, serialNumber, notBefore, notAfter, subject, publicKeyInfo
            )
    
            try {
                certBuilder.addExtension(Extension.basicConstraints, true, BasicConstraints(false))
                certBuilder.addExtension(Extension.keyUsage, true,
                    KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment))
                certBuilder.addExtension(Extension.extendedKeyUsage, true,
                    ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth))
            } catch (e: Exception) {}
    
            val signer = JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.private)
            val certHolder = certBuilder.build(signer)
            return JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder)
        }
    
        private suspend fun sendPairingRequest() {
            withContext(Dispatchers.IO) {
                try {
                    val message = protocol.buildPairingRequest("androidtv-remote", "Android Phone")
                    val complete = addLengthPrefix(message)
    
                    Log.d(TAG, "→ Pairing Request: ${complete.joinToString(" ") { "%02X".format(it) }}")
                    outputStream?.write(complete)
                    // Don't flush - send all messages together
                    Log.d(TAG, "✓ Written to buffer")
                } catch (e: Exception) {
                    Log.e(TAG, "❌ Failed", e)
                    throw e
                }
            }
        }
    
        private suspend fun sendOptions() {
            withContext(Dispatchers.IO) {
                try {
                    val message = protocol.buildOptions()
                    val complete = addLengthPrefix(message)
    
                    Log.d(TAG, "→ OPTIONS: ${complete.joinToString(" ") { "%02X".format(it) }}")
                    outputStream?.write(complete)
                    // Don't flush
                    Log.d(TAG, "✓ Written to buffer")
                } catch (e: Exception) {
                    Log.e(TAG, "❌ Failed", e)
                }
            }
        }
    
        private suspend fun sendConfiguration() {
            withContext(Dispatchers.IO) {
                try {
                    val message = protocol.buildConfiguration()
                    val complete = addLengthPrefix(message)
    
                    Log.d(TAG, "→ CONFIGURATION: ${complete.joinToString(" ") { "%02X".format(it) }}")
                    outputStream?.write(complete)
                    // Don't flush - will flush all together
                    Log.d(TAG, "✓ Written to buffer")
                } catch (e: Exception) {
                    Log.e(TAG, "❌ Failed", e)
                }
            }
        }
    
        private suspend fun waitForPairingCode() {
            withContext(Dispatchers.IO) {
                try {
                    Log.d(TAG, "Reading TV response...")
                    sslSocket?.soTimeout = 5000 // 5 seconds
    
                    // Read length byte
                    val lengthByte = inputStream?.read() ?: -1
                    if (lengthByte == -1) {
                        Log.d(TAG, "No immediate response from TV")
                        // This is OK - TV shows code on screen
                        withContext(Dispatchers.Main) {
                            listener.onPairingCodeRequired("")
                        }
                        return@withContext
                    }
    
                    val messageLength = lengthByte and 0xFF
                    Log.d(TAG, "← Response length: $messageLength bytes")
    
                    if (messageLength > 0) {
                        // Read the message
                        val messageData = ByteArray(messageLength)
                        var totalRead = 0
    
                        while (totalRead < messageLength) {
                            val bytesRead = inputStream?.read(messageData, totalRead, messageLength - totalRead) ?: -1
                            if (bytesRead == -1) break
                            totalRead += bytesRead
                        }
    
                        if (totalRead == messageLength) {
                            Log.d(TAG, "← Message: ${messageData.joinToString(" ") { "%02X".format(it) }}")
                        }
                    }
    
                    // The pairing code is shown ON THE TV SCREEN
                    // Not sent to us over network!
                    Log.d(TAG, "✓✓✓ CHECK YOUR TV SCREEN FOR PAIRING CODE ✓✓✓")
                    withContext(Dispatchers.Main) {
                        listener.onPairingCodeRequired("")
                    }
    
                } catch (e: Exception) {
                    Log.d(TAG, "Response read complete")
                    // Still OK - code is on TV
                    withContext(Dispatchers.Main) {
                        listener.onPairingCodeRequired("")
                    }
                }
            }
        }
    
        fun sendPairingCode(code: String) {
            scope.launch {
                try {
                    Log.d(TAG, "→ Sending user code: $code")
    
                    // Need to compute alpha (challenge response)
                    val alpha = computeAlpha(code)
                    val message = protocol.buildSecret(alpha)
                    val complete = addLengthPrefix(message)
    
                    Log.d(TAG, "→ SECRET: ${complete.joinToString(" ") { "%02X".format(it) }}")
                    outputStream?.write(complete)
                    outputStream?.flush()
                    Log.d(TAG, "✓ Sent")
    
                    // Wait for ACK
                    delay(500)
                    Log.d(TAG, "✓✓✓ PAIRING COMPLETE! ✓✓✓")
                    isConnected = true
                    withContext(Dispatchers.Main) {
                        listener.onPairingSuccess()
                        listener.onConnectionEstablished()
                    }
                } catch (e: Exception) {
                    Log.e(TAG, "❌ Failed", e)
                }
            }
        }
    
        private fun computeAlpha(hexCode: String): ByteArray {
            // Convert hex code to bytes
            val codeBytes = hexCode.chunked(2)
                .map { it.toInt(16).toByte() }
                .toByteArray()
    
            // Get certificates
            val clientCert = sslSocket?.session?.localCertificates?.get(0)
            val serverCert = sslSocket?.session?.peerCertificates?.get(0)
    
            if (clientCert != null && serverCert != null) {
                try {
                    // Compute SHA-256 hash as in PairingChallengeResponse
                    val digest = MessageDigest.getInstance("SHA-256")
    
                    // Add public key data
                    val clientPubKey = clientCert.publicKey
                    val serverPubKey = serverCert.publicKey
    
                    // Simple version: just hash the code bytes
                    digest.update(codeBytes)
    
                    return digest.digest()
                } catch (e: Exception) {
                    Log.e(TAG, "Alpha computation error", e)
                }
            }
    
            // Fallback: return code bytes
            return codeBytes
        }
    
        /**
         * Add single-byte length prefix (as in reference app)
         */
        private fun addLengthPrefix(message: ByteArray): ByteArray {
            val result = ByteArray(message.size + 1)
            result[0] = message.size.toByte()
            System.arraycopy(message, 0, result, 1, message.size)
            return result
        }
    
        fun disconnect() {
            scope.launch { cleanup() }
            scope.cancel()
        }
    
        private fun cleanup() {
            try {
                inputStream?.close()
                outputStream?.close()
                sslSocket?.close()
                inputStream = null
                outputStream = null
                sslSocket = null
                isConnected = false
            } catch (e: Exception) {}
        }
    }
  

class AndroidTVRemoteProtocol {
    companion object {
        private const val TAG = "AndroidTVProtocol"

        private const val WIRE_TYPE_VARINT = 0
        private const val WIRE_TYPE_LENGTH_DELIMITED = 2

        // Message type field numbers in PairingMessage
        private const val FIELD_PAIRING_REQUEST = 1
        private const val FIELD_PAIRING_OPTION = 2
        private const val FIELD_PAIRING_CONFIGURATION = 3
        private const val FIELD_PAIRING_SECRET = 4
        private const val FIELD_STATUS = 200
        private const val FIELD_PROTOCOL_VERSION = 201
    }

    /**
     * Build pairing request wrapped in PairingMessage
     */
    fun buildPairingRequest(serviceName: String, clientName: String): ByteArray {
        val pairingRequest = ByteArrayOutputStream()

        // Field 1: service_name
        writeString(pairingRequest, 1, serviceName)
        // Field 2: client_name
        writeString(pairingRequest, 2, clientName)

        // Wrap in PairingMessage
        return wrapInPairingMessage(FIELD_PAIRING_REQUEST, pairingRequest.toByteArray())
    }

    /**
     * Build OPTIONS wrapped in PairingMessage
     * PairingOption has:
     *   field 1: preferred_role (varint)
     *   field 2: input_encodings (repeated, length-delimited)
     */
    fun buildOptions(): ByteArray {
        val pairingOption = ByteArrayOutputStream()

        // Field 1: preferred_role = ROLE_TYPE_INPUT (1)
        writeVarint(pairingOption, (1 shl 3) or WIRE_TYPE_VARINT)
        writeVarint(pairingOption, 1)

        // Field 2: input_encodings (REPEATED - write encoding directly, not nested)
        val encoding = buildPairingEncoding()
        // Write as repeated field (just write field 2 with encoding bytes)
        writeVarint(pairingOption, (2 shl 3) or WIRE_TYPE_LENGTH_DELIMITED)
        writeVarint(pairingOption, encoding.size)
        pairingOption.write(encoding)

        return wrapInPairingMessage(FIELD_PAIRING_OPTION, pairingOption.toByteArray())
    }

    /**
     * Build CONFIGURATION wrapped in PairingMessage
     */
    fun buildConfiguration(): ByteArray {
        val pairingConfig = ByteArrayOutputStream()

        // Field 1: client_role
        writeVarint(pairingConfig, (1 shl 3) or WIRE_TYPE_VARINT)
        writeVarint(pairingConfig, 1) // ROLE_TYPE_INPUT

        // Field 2: encoding
        val encoding = buildPairingEncoding()
        writeVarint(pairingConfig, (2 shl 3) or WIRE_TYPE_LENGTH_DELIMITED)
        writeVarint(pairingConfig, encoding.size)
        pairingConfig.write(encoding)

        return wrapInPairingMessage(FIELD_PAIRING_CONFIGURATION, pairingConfig.toByteArray())
    }

    /**
     * Build PairingEncoding message
     * Field 1: type = ENCODING_TYPE_HEXADECIMAL (1)
     * Field 2: symbol_length = 6
     */
    private fun buildPairingEncoding(): ByteArray {
        val encoding = ByteArrayOutputStream()

        // Field 1: encoding_type = HEXADECIMAL (1)
        writeVarint(encoding, (1 shl 3) or WIRE_TYPE_VARINT)
        writeVarint(encoding, 1)

        // Field 2: symbol_length = 6
        writeVarint(encoding, (2 shl 3) or WIRE_TYPE_VARINT)
        writeVarint(encoding, 6)

        return encoding.toByteArray()
    }

    /**
     * Build secret wrapped in PairingMessage
     */
    fun buildSecret(alpha: ByteArray): ByteArray {
        val pairingSecret = ByteArrayOutputStream()

        // Field 1: secret bytes
        writeBytes(pairingSecret, 1, alpha)

        return wrapInPairingMessage(FIELD_PAIRING_SECRET, pairingSecret.toByteArray())
    }

    /**
     * Wrap message in PairingMessage with status and protocol version
     */
    private fun wrapInPairingMessage(fieldNumber: Int, payload: ByteArray): ByteArray {
        val wrapper = ByteArrayOutputStream()

        // Field X: the actual message (pairing_request, option, etc.)
        writeVarint(wrapper, (fieldNumber shl 3) or WIRE_TYPE_LENGTH_DELIMITED)
        writeVarint(wrapper, payload.size)
        wrapper.write(payload)

        // Field 200: status = STATUS_OK (1)
        writeVarint(wrapper, (FIELD_STATUS shl 3) or WIRE_TYPE_VARINT)
        writeVarint(wrapper, 1)

        // Field 201: protocol_version = 2
        writeVarint(wrapper, (FIELD_PROTOCOL_VERSION shl 3) or WIRE_TYPE_VARINT)
        writeVarint(wrapper, 2)

        return wrapper.toByteArray()
    }

    /**
     * Parse message - single byte length prefix
     */
    fun parseMessage(data: ByteArray): ProtocolMessage? {
        if (data.isEmpty()) return null

        try {
            val messageLength = data[0].toInt() and 0xFF
            Log.d(TAG, "Length: $messageLength bytes")

            if (data.size < messageLength + 1) {
                Log.w(TAG, "⚠ Incomplete")
                return null
            }

            val messageData = data.copyOfRange(1, messageLength + 1)
            Log.d(TAG, "Message: ${messageData.joinToString(" ") { "%02X".format(it) }}")

            return ProtocolMessage.createUnknown()

        } catch (e: Exception) {
            Log.e(TAG, "❌ Parse error: ${e.message}", e)
            return null
        }
    }

    private fun writeString(output: OutputStream, fieldNumber: Int, value: String) {
        val bytes = value.toByteArray(Charsets.UTF_8)
        writeBytes(output, fieldNumber, bytes)
    }

    private fun writeBytes(output: OutputStream, fieldNumber: Int, value: ByteArray) {
        writeVarint(output, (fieldNumber shl 3) or WIRE_TYPE_LENGTH_DELIMITED)
        writeVarint(output, value.size)
        output.write(value)
    }

    private fun writeVarint(output: OutputStream, value: Int) {
        var v = value
        while (v and 0x7F.inv() != 0) {
            output.write((v and 0x7F) or 0x80)
            v = v ushr 7
        }
        output.write(v and 0x7F)
    }

    class ProtocolMessage private constructor(
        val type: MessageType,
        val secret: String? = null
    ) {

        enum class MessageType {
            SECRET,
            SECRET_ACK,
            UNKNOWN
        }

        companion object {
            fun createSecret(secret: String): ProtocolMessage {
                return ProtocolMessage(MessageType.SECRET, secret = secret)
            }

            fun createSecretAck(): ProtocolMessage {
                return ProtocolMessage(MessageType.SECRET_ACK)
            }

            fun createUnknown(): ProtocolMessage {
                return ProtocolMessage(MessageType.UNKNOWN)
            }
        }
    }
}
android ssl-certificate android-tv remote-control smart-tv