Android-Smart-TV-Gerät zeigt keinen Kopplungscode anAndroid

Forum für diejenigen, die für Android programmieren
Anonymous
 Android-Smart-TV-Gerät zeigt keinen Kopplungscode an

Post by Anonymous »

Ich arbeite an der Android TV Remote App. Ich verwende Connect SDK, um Geräte zu erkennen. Um die Android-App mit Smart TV zu koppeln, verwende ich eine direkte SSL-Verbindung zur IP-Adresse des Fernsehers an Port 6467 und für die Kopplung sende ich Protobuf-Nachrichten (Pairing-Anfrage → Optionen → Konfiguration) und erwarte, dass der Fernseher auf dem Bildschirm einen 6-stelligen Code für die Kopplung anzeigt, der aber überhaupt nicht angezeigt wird. Hier ist mein Codeausschnitt:

Code: Select all

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)
}
}
}
}

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post