Chr*_*lla 6 android remote-control
我的任务是为 android mobile 创建一个应用程序来控制 Android TV,最好是任何应用程序之外的仪表板/登陆页面(包括设置)。是通过蓝牙还是 wifi 并不重要,尽管我发现蓝牙是不可能的,因为需要 HID 配置文件,并且该配置文件仅在 API 28 上可用(我需要从 API 19 开始支持)
Play 商店中有一些应用程序已经具有此功能。大多数通过 Wifi 连接到 Android TV,也与它配对。
通过分析 APK 文件,我发现了一些选项,即
有些使用 connectSDK库
其他人使用的似乎是我似乎找不到的本地 google 包
import com.google.android.tv.support.remote.Discovery;
import com.google.android.tv.support.remote.core.Client;
import com.google.android.tv.remote.BuildInfo;
Run Code Online (Sandbox Code Playgroud)我发现几年前也可以使用 Anymote 协议,但该协议仅适用于 Google TV,不适用于 Android TV。
我现在面临的问题是connectSDK库没有得到维护并且不包含任何用于 Android TV 连接的代码。本地 google 包在任何地方都找不到,不确定它是否包含在特定的 Jar 文件中,或者可能是一些模糊/隐藏的依赖项?
我可以尝试使用 Android TV 创建到特定套接字的连接,例如,我知道ServiceTypeis"_androidtvremote._tcp."并且端口号是6466。但我不确定实现这一点的最佳方法是什么。
我正在寻找的是我如何解决这个问题的一些指示或想法。也许也有一些参考。
Aym*_*Kdn 15
EDIT on December 2021: I created a new documentation for the new protocol v2.
\nEDIT on September 2021: Google is deploying a new version of the "Android TV Remote Control" (from v4.x to v5), and this version is not compatible with the legacy pairing system. For now it\'s necessary to keep a version < 5 to make it work.
\nWe spent some time to find how to connect and control an Android/Google TV (by reverse engineering), and I\'m sharing here the result of our findings. For a more recent/updated version, you can check this wiki page.
\nI develop in PHP so I\'ll share the code in PHP (the Java code can be found by decompiling some Android apps using https://github.com/skylot/jadx)
\nThanks to @hubertlejaune for his tremendous help.
\nThe Android TV (aka server in this document) should have 2 open ports: 6466 and 6467.
To know more about the Android TV, we can enter the below Linux command:
\n\n\nopenssl s_client -connect SERVER_IP:6467 -prexit -state -debug
\n
Which will return some information, including the server\'s public certificate.
\nIf you only want the server\'s public certificate:
\n\n\nopenssl s_client -showcerts -connect SERVER_IP:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem
\n
The pairing protocol will happen on port 6467.
\nIt\'s required to generate our own (client) certificate.
\nIn PHP we can do it with the below code:
\n<?php\n// the commande line is: php generate_key.php > client.pem\n\n// certificate details (Distinguished Name)\n// (OpenSSL applies defaults to missing fields)\n$dn = array(\n "commonName" => "atvremote",\n "countryName" => "US",\n "stateOrProvinceName" => "California",\n "localityName" => "Montain View",\n "organizationName" => "Google Inc.",\n "organizationalUnitName" => "Android",\n "emailAddress" => "example@google.com"\n);\n\n// create certificate which is valid for ~10 years\n$privkey = openssl_pkey_new();\n$cert = openssl_csr_new($dn, $privkey);\n$cert = openssl_csr_sign($cert, null, $privkey, 3650);\n\n// export public key\nopenssl_x509_export($cert, $out);\necho $out;\n\n// export private key\n$passphrase = null;\nopenssl_pkey_export($privkey, $out, $passphrase);\necho $out;\nRun Code Online (Sandbox Code Playgroud)\nIt will generate a file called client.pem that contains both the public and the private keys for our client.
You need to open a TLS/SSL connection to the server using port 6467.
\nIn PHP, you could use https://github.com/reactphp/socket:
\n<?php\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Connector;\nuse React\\Socket\\SecureConnector;\nuse React\\Socket\\ConnectionInterface;\n\nrequire __DIR__ . \'/./vendor/autoload.php\';\n\n$host = \'SERVER_IP\';\n$loop = Factory::create();\n$tcpConnector = new React\\Socket\\TcpConnector($loop);\n$dnsResolverFactory = new React\\Dns\\Resolver\\Factory();\n$dns = $dnsResolverFactory->createCached(\'8.8.8.8\', $loop);\n$dnsConnector = new React\\Socket\\DnsConnector($tcpConnector, $dns);\n\n$connector = new SecureConnector($dnsConnector, $loop, array(\n \'allow_self_signed\' => true,\n \'verify_peer\' => false,\n \'verify_peer_name\' => false,\n \'dns\' => false,\n \'local_cert\' => \'client.pem\'\n));\n\n$connector->connect(\'tls://\' . $host . \':6467\')->then(function (ConnectionInterface $connection) use ($host) {\n $connection->on(\'data\', function ($data) use ($connection) {\n $dataLen = strlen($data);\n echo "data recv => ".$data." (".strlen($data).")\\n";\n // deal with the messages received from the server\n });\n \n // below we can send the first message\n $connection->write(/* first message here */);\n}, \'printf\');\n\n$loop->run();\n?>\nRun Code Online (Sandbox Code Playgroud)\n\xe2\x9a\xa0\xef\xb8\x8f Attention, each message is sent as a JSON string, but with two components/parts:
\nAs soon as we are connected to the server, we send a PAIRING_REQUEST(10) message (type = 10).
The first message to send is:
\n\n\n{"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"CLIENT_NAME"},"type":10,"status":200}
\n
The server returns a PAIRING_REQUEST_ACK(11) message with type is 11 and status is 200:
\n\n{"protocol_version":1,"payload":{},"type":11,"status":200}
\n
Then the client replies with a OPTIONS(20) message (type = 20):
\n\n{"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}
\n
The server returns a OPTIONS(20) message with type is 20 and status is 200.
Then the client replies with a CONFIGURATION(30) message (type = 30):
\n\n{"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}
\n
The server returns a CONFIGURATION_ACK(31) message with type is 31 and status is 200.
代码出现在电视屏幕上!
\n然后客户端回复一条SECRET(40)消息 ( type= 40):
\n\n{“protocol_version”:1,“payload”:{“secret”:“encodedSecret”},“type”:40,“status”:200}
\n
此时,电视屏幕上会显示4个字符的代码(例如4D35)。
\n为了找到encodedSecret:
modulus to the hash;exponent to the hash;modulus to the hash;exponent to the hash;35).然后将哈希结果编码为 Base64。
\n服务器返回类型为 is和is的SECRET_ACK(41)消息,以及允许验证 \xe2\x80\x93 的编码秘密,我们没有尝试对其进行解码,但它可能是前 2 个代码的字符:41status200
\n\n{“protocol_version”:1,“payload”:{“secret”:“encodedSecretAck”},“type”:41,“status”:200}
\n
(你可以找到一些产生几乎相同结果的Java 代码)
\n下面是相关的PHP代码:
\n<?php\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Connector;\nuse React\\Socket\\SecureConnector;\nuse React\\Socket\\ConnectionInterface;\n\nrequire __DIR__ . \'/./vendor/autoload.php\';\n\n$host = \'SERVER_IP\';\n$loop = Factory::create();\n$tcpConnector = new React\\Socket\\TcpConnector($loop);\n$dnsResolverFactory = new React\\Dns\\Resolver\\Factory();\n$dns = $dnsResolverFactory->createCached(\'8.8.8.8\', $loop);\n$dnsConnector = new React\\Socket\\DnsConnector($tcpConnector, $dns);\n\n// get the server\'s public certificate\nexec("openssl s_client -showcerts -connect ".escapeshellcmd($host).":6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem");\n\n$connector = new SecureConnector($dnsConnector, $loop, array(\n \'allow_self_signed\' => true,\n \'verify_peer\' => false,\n \'verify_peer_name\' => false,\n \'dns\' => false,\n \'local_cert\' => \'client.pem\'\n));\n\n// return the message\'s length on 4 bytes\nfunction getLen($len) {\n return chr($len>>24 & 0xFF).chr($len>>16 & 0xFF).chr($len>>8 & 0xFF).chr($len & 0xFF);\n}\n\n// connect to the server\n$connector->connect(\'tls://\' . $host . \':6467\')->then(function (ConnectionInterface $connection) use ($host) {\n $connection->on(\'data\', function ($data) use ($connection) {\n $dataLen = strlen($data);\n echo "data recv => ".$data." (".strlen($data).")\\n";\n\n // the first response from the server is the message\'s size on 4 bytes (that looks like a char to convert to decimal) \xe2\x80\x93 we can ignore it\n // only look at messages longer than 4 bytes\n if ($dataLen > 4) {\n // decode the JSON string\n $res = json_decode($data);\n // check the status is 200\n if ($res->status === 200) {\n // check at which step we are\n switch($res->type) {\n case 11:{\n // message to send:\n // {"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}\n $json = new stdClass();\n $json->protocol_version = 1;\n $json->payload = new stdClass();\n $json->payload->output_encodings = [];\n $encoding = new stdClass();\n $encoding->symbol_length = 4;\n $encoding->type = 3;\n array_push($json->payload->output_encodings, $encoding);\n $json->payload->input_encodings = [];\n $encoding = new stdClass();\n $encoding->symbol_length = 4;\n $encoding->type = 3;\n array_push($json->payload->input_encodings, $encoding);\n $json->payload->preferred_role = 1;\n $json->type = 20;\n $json->status = 200;\n $payload = json_encode($json);\n $payloadLen = strlen($payload);\n $connection->write(getLen($payloadLen));\n $connection->write($payload);\n break;\n }\n case 20:{\n // message to send:\n // {"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}\n $json = new stdClass();\n $json->protocol_version = 1;\n $json->payload = new stdClass();\n $json->payload->encoding = new stdClass();\n $json->payload->encoding->symbol_length = 4;\n $json->payload->encoding->type = 3;\n $json->payload->client_role = 1;\n $json->type = 30;\n $json->status = 200;\n $payload = json_encode($json);\n $payloadLen = strlen($payload);\n $connection->write(getLen($payloadLen));\n $connection->write($payload);\n break;\n }\n case 31:{\n // when we arrive here, the TV screen displays a code with 4 characters\n // message to send:\n // {"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}\n $json = new stdClass();\n $json->protocol_version = 1;\n $json->payload = new stdClass();\n // get the code... here we\'ll let the user to enter it in the console\n $code = readline("Code: ");\n\n // get the client\'s certificate\n $clientPub = openssl_get_publickey(file_get_contents("client.pem"));\n $clientPubDetails = openssl_pkey_get_details($clientPub);\n // get the server\'s certificate\n $serverPub = openssl_get_publickey(file_get_contents("public.key"));\n $serverPubDetails = openssl_pkey_get_details($serverPub);\n\n // get the client\'s certificate modulus\n $clientModulus = $clientPubDetails[\'rsa\'][\'n\'];\n // get the client\'s certificate exponent\n $clientExponent = $clientPubDetails[\'rsa\'][\'e\'];\n // get the server\'s certificate modulus\n $serverModulus = $serverPubDetails[\'rsa\'][\'n\'];\n // get the server\'s certificate exponent\n $serverExponent = $serverPubDetails[\'rsa\'][\'e\'];\n\n // use SHA-256\n $ctxHash = hash_init(\'sha256\');\n hash_update($ctxHash, $clientModulus);\n hash_update($ctxHash, $clientExponent);\n hash_update($ctxHash, $serverModulus);\n hash_update($ctxHash, $serverExponent);\n // only keep the last two characters of the code\n $codeBin = hex2bin(substr($code, 2));\n hash_update($ctxHash, $codeBin);\n $alpha = hash_final($ctxHash, true);\n \n // encode in base64\n $json->payload->secret = base64_encode($alpha);\n $json->type = 40;\n $json->status = 200;\n $payload = json_encode($json);\n $payloadLen = strlen($payload);\n\n $connection->write(getLen($payloadLen));\n $connection->write($payload);\n break;\n }\n }\n }\n }\n });\n\n // send the first message to the server\n // {"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"TEST"},"type":10,"status":200}\n $json = new stdClass();\n $json->protocol_version = 1;\n $json->payload = new stdClass();\n $json->payload->service_name = "androidtvremote";\n $json->payload->client_name = "interface Web";\n $json->type = 10;\n $json->status = 200;\n $payload = json_encode($json);\n $payloadLen = strlen($payload);\n\n // send the message size\n $connection->write(getLen($payloadLen));\n // send the message\n $connection->write($payload);\n}, \'printf\');\n\n$loop->run();\n?>\nRun Code Online (Sandbox Code Playgroud)\n现在客户端已与服务器配对,我们将使用端口 6466发送命令。
\n请注意,我们将使用字节数组作为命令。
必须发送初始消息:
\n\n\n[1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116]
\n
服务器将响应一个字节数组,该数组应以[1,7,0
您必须发送两条消息才能执行一条命令。
\n格式为:
\n\n\n[1,2,0,{大小=16},0,0,0,0,0,0,0, {计数器} ,0,0,0, {按=0} ,0,0,0,{ KEYCODE}]
\n
\n[1,2,0,{SIZE=16},0,0,0,0,0,0,0,{COUNTER+1},0,0,0,{RELEASE=1}, 0,0,0,{KEYCODE}]
可以在https://developer.android.com/reference/android/view/KeyEvent{KEYCODE}上找到。
例如,如果我们想发送VOLUME_UP:
\n\n[1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]
\n
\n[1,2,0, 16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]
这里是一些 PHP 代码:
\n<?php\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Connector;\nuse React\\Socket\\SecureConnector;\nuse React\\Socket\\ConnectionInterface;\n\nrequire __DIR__ . \'/./vendor/autoload.php\';\n\n$host = \'SERVER_IP\';\n$loop = Factory::create();\n$tcpConnector = new React\\Socket\\TcpConnector($loop);\n$dnsResolverFactory = new React\\Dns\\Resolver\\Factory();\n$dns = $dnsResolverFactory->createCached(\'8.8.8.8\', $loop);\n$dnsConnector = new React\\Socket\\DnsConnector($tcpConnector, $dns);\n\n$connector = new SecureConnector($dnsConnector, $loop, array(\n \'allow_self_signed\' => true,\n \'verify_peer\' => false,\n \'verify_peer_name\' => false,\n \'dns\' => false,\n \'local_cert\' => \'client.pem\'\n));\n\n// convert the array of bytes\nfunction toMsg($arr) {\n $chars = array_map("chr", $arr);\n return join($chars);\n}\n\n// connect to the server\n$connector->connect(\'tls://\' . $host . \':6466\')->then(function (ConnectionInterface $connection) use ($host) {\n $connection->on(\'data\', function ($data) use ($connection) {\n // convert the data received to an array of bytes\n $dataLen = strlen($data);\n $arr = [];\n for ($i=0; $i<$dataLen;$i++) {\n $arr[] = ord($data[$i]);\n }\n $str = "[".implode(",", $arr)."]";\n echo "data recv => ".$data." ".$str." (".strlen($data).")\\n";\n\n // if we receive [1,20,0,0] it means the server sent a ping\n if (strpos($str, "[1,20,0,0]") === 0) {\n // we can reply with a PONG [1,21,0,0] if we want\n // $connection->write(toMsg([1,21,0,0]));\n }\n else if (strpos($str, "[1,7,0,") === 0) {\n // we can send the command, here it\'s a VOLUME_UP\n $connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]));\n $connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]));\n }\n });\n\n // send the first message (configuration) to the server\n $arr = [1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116];\n $connection->write(toMsg($arr));\n}, \'printf\');\n\n$loop->run();\n?>\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
3867 次 |
| 最近记录: |