2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.androidtv.internal.protocol.shieldtv;
15 import static org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConstants.*;
17 import java.util.ArrayList;
18 import java.util.Base64;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.LinkedHashMap;
22 import java.util.List;
24 import java.util.Map.Entry;
26 import javax.xml.bind.DatatypeConverter;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * Class responsible for parsing incoming ShieldTV messages. Calls back to an object implementing the
34 * ShieldTVMessageParserCallbacks interface.
36 * Adapted from Lutron Leap binding
38 * @author Ben Rosenblum - Initial contribution
42 public class ShieldTVMessageParser {
43 private final Logger logger = LoggerFactory.getLogger(ShieldTVMessageParser.class);
45 private final ShieldTVConnectionManager callback;
47 public ShieldTVMessageParser(ShieldTVConnectionManager callback) {
48 this.callback = callback;
51 public void handleMessage(String msg) {
52 if (msg.trim().isEmpty()) {
53 return; // Ignore empty lines
56 String thingId = callback.getThingID();
57 String hostName = callback.getHostName();
58 logger.trace("{} - Received ShieldTV message from: {} - Message: {}", thingId, hostName, msg);
60 callback.validMessageReceived();
62 char[] charArray = msg.toCharArray();
65 // All lengths are little endian when larger than 0xff
66 if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_SHORTNAME, 8)) {
67 // Pre-login Hostname of Shield Replied
68 // 080a 12 1408e807 12 0f08e807 12 LEN Hostname 18d7fd04 180a
69 // 080a 12 1d08e807 12 180801 12 LEN Hostname 18d7fd04 180a
70 // 080a 12 2208e807 12 1d08e807 12 LEN Hostname 18d7fd04 180a
71 // Each chunk ends in 12
72 // 4th chunk represent length of the name.
73 // 5th chunk is the name
77 StringBuilder hostname = new StringBuilder();
79 st = "" + charArray[i] + "" + charArray[i + 1];
80 if (DELIMITER_12.equals(st)) {
85 st = "" + charArray[i] + "" + charArray[i + 1];
87 int length = Integer.parseInt(st, 16) * 2;
89 for (; i < current + length; i = i + 2) {
90 st = "" + charArray[i] + "" + charArray[i + 1];
93 logger.trace("{} - Shield Hostname: {} {}", thingId, hostname, length);
94 String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
95 logger.debug("{} - Shield Hostname Encoded: {}", thingId, encHostname);
96 callback.setHostName(encHostname);
97 } else if (msg.startsWith(MESSAGE_HOSTNAME)) {
98 // Longer hostname reply
99 // 080b 12 5b08b510 12 TOTALLEN? 0a LEN Hostname 12 LEN IPADDR 18 9b46 22 LEN DeviceID 2a LEN arm64-v8a
100 // 2a LEN armeabi-v7a 2a LEN armeabi 180b
101 // It's possible for there to be more or less of the arm lists
102 logger.trace("{} - Longer Hostname Reply", thingId);
108 StringBuilder hostname = new StringBuilder();
109 StringBuilder ipAddress = new StringBuilder();
110 StringBuilder deviceId = new StringBuilder();
111 StringBuilder arch = new StringBuilder();
113 String st = "" + charArray[i] + "" + charArray[i + 1];
115 if (DELIMITER_0A.equals(st)) {
118 st = "" + charArray[i] + "" + charArray[i + 1];
119 length = Integer.parseInt(st, 16) * 2;
124 for (; i < current + length; i = i + 2) {
125 st = "" + charArray[i] + "" + charArray[i + 1];
129 st = "" + charArray[i] + "" + charArray[i + 1];
131 if (DELIMITER_12.equals(st)) {
135 st = "" + charArray[i] + "" + charArray[i + 1];
136 length = Integer.parseInt(st, 16) * 2;
141 for (; i < current + length; i = i + 2) {
142 st = "" + charArray[i] + "" + charArray[i + 1];
143 ipAddress.append(st);
147 st = "" + charArray[i] + "" + charArray[i + 1];
148 if (DELIMITER_18.equals(st)) {
149 while (!DELIMITER_22.equals(st)) {
151 st = "" + charArray[i] + "" + charArray[i + 1];
155 st = "" + charArray[i] + "" + charArray[i + 1];
156 if (DELIMITER_22.equals(st)) {
161 st = "" + charArray[i] + "" + charArray[i + 1];
162 length = Integer.parseInt(st, 16) * 2;
167 for (; i < current + length; i = i + 2) {
168 st = "" + charArray[i] + "" + charArray[i + 1];
174 st = "" + charArray[i] + "" + charArray[i + 1];
175 if (DELIMITER_2A.equals(st)) {
176 while (DELIMITER_2A.equals(st)) {
178 st = "" + charArray[i] + "" + charArray[i + 1];
179 length = Integer.parseInt(st, 16) * 2;
182 for (; i < current + length; i = i + 2) {
183 st = "" + charArray[i] + "" + charArray[i + 1];
186 st = "" + charArray[i] + "" + charArray[i + 1];
187 if (DELIMITER_2A.equals(st)) {
192 String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
193 String encIpAddress = ShieldTVRequest.encodeMessage(ipAddress.toString());
194 String encDeviceId = ShieldTVRequest.encodeMessage(deviceId.toString());
195 String encArch = ShieldTVRequest.encodeMessage(arch.toString());
196 logger.debug("{} - Hostname: {} - ipAddress: {} - deviceId: {} - arch: {}", thingId, encHostname,
197 encIpAddress, encDeviceId, encArch);
198 callback.setHostName(encHostname);
199 callback.setDeviceID(encDeviceId);
200 callback.setArch(encArch);
201 } else if (APP_START_SUCCESS.equals(msg)) {
202 // App successfully started
203 logger.debug("{} - App started successfully", thingId);
204 } else if (APP_START_FAILED.equals(msg)) {
205 // App failed to start
206 logger.debug("{} - App failed to start", thingId);
207 } else if (msg.startsWith(MESSAGE_APPDB) && msg.startsWith(MESSAGE_APPDB_FULL, 12)) {
208 // Massive dump of currently installed apps
209 // 08f10712 d81f 080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
210 // --------------08XX12 where XX is not 01 are individual updates and should be ignored
211 Map<String, String> appNameDB = new HashMap<>();
212 Map<String, String> appURLDB = new HashMap<>();
218 StringBuilder appSBPrepend = new StringBuilder();
219 StringBuilder appSBDN = new StringBuilder();
221 // Load default apps that don't get sent in payload
223 appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
224 appURLDB.put("com.google.android.tvlauncher", "");
226 appNameDB.put("com.google.android.katniss", "Google app for Android TV");
227 appURLDB.put("com.google.android.katniss", "");
229 appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
230 appURLDB.put("com.google.android.katnisspx", "");
232 appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
233 appURLDB.put("com.google.android.backdrop", "");
235 // Packet will end with 300118f107 after last entry
237 while (i < msg.length() - 10) {
238 StringBuilder appSBName = new StringBuilder();
239 StringBuilder appSBURL = new StringBuilder();
241 // There are instances such as plex where multiple apps are sent as part of the same payload
242 // This is identified when 12 is the beginning of the set
244 st = "" + charArray[i] + "" + charArray[i + 1];
246 if (!DELIMITER_12.equals(st)) {
247 appSBPrepend = new StringBuilder();
248 appSBDN = new StringBuilder();
253 // Usually 10 in length but can be longer or shorter so look for 0a twice
255 st = "" + charArray[i] + "" + charArray[i + 1];
256 appSBPrepend.append(st);
258 } while (!DELIMITER_0A.equals(st));
260 st = "" + charArray[i] + "" + charArray[i + 1];
261 appSBPrepend.append(st);
263 } while (!DELIMITER_0A.equals(st));
264 st = "" + charArray[i] + "" + charArray[i + 1];
266 // Look for a third 0a, but only if 12 is not down the line
267 // If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
268 String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
269 if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
270 appSBPrepend.append(st);
272 st = "" + charArray[i] + "" + charArray[i + 1];
276 length = Integer.parseInt(st, 16) * 2;
279 for (; i < current + length; i = i + 2) {
280 st = "" + charArray[i] + "" + charArray[i + 1];
284 logger.trace("Second Entry");
289 i += 2; // 12 delimiter
290 st = "" + charArray[i] + "" + charArray[i + 1];
292 length = Integer.parseInt(st, 16) * 2;
294 for (; i < current + length; i = i + 2) {
295 st = "" + charArray[i] + "" + charArray[i + 1];
296 appSBName.append(st);
299 // There are times where there is padding here for no reason beyond the specified length.
300 // Proceed forward until we get to the 22 delimiter
302 st = "" + charArray[i] + "" + charArray[i + 1];
303 while (!DELIMITER_22.equals(st)) {
305 st = "" + charArray[i] + "" + charArray[i + 1];
309 i += 2; // 22 delimiter
310 st = "" + charArray[i] + "" + charArray[i + 1];
312 length = Integer.parseInt(st, 16) * 2;
314 for (; i < current + length; i = i + 2) {
315 st = "" + charArray[i] + "" + charArray[i + 1];
318 st = "" + charArray[i] + "" + charArray[i + 1];
319 if (!DELIMITER_12.equals(st)) {
320 i += 4; // terminates 2801
322 String appPrepend = appSBPrepend.toString();
323 String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
324 String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
325 String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
326 logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
328 appNameDB.put(appDN, appName);
329 appURLDB.put(appDN, appURL);
332 Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
333 List<String> valueList = new ArrayList<>();
334 for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
335 valueList.add(entry.getValue());
337 Collections.sort(valueList);
338 for (String str : valueList) {
339 for (Entry<String, String> entry : appNameDB.entrySet()) {
340 if (entry.getValue().equals(str)) {
341 sortedAppNameDB.put(entry.getKey(), str);
346 logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
347 appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
348 callback.setAppDB(sortedAppNameDB, appURLDB);
350 logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
351 appNameDB.toString(), appURLDB.toString());
353 } else if (msg.startsWith(MESSAGE_APPDB)) {
354 logger.debug("{} - Individual app update ignored {}", thingId, msg);
355 } else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
356 // This has something to do with successful command response, maybe.
357 logger.trace("{} - Good Command Response", thingId);
358 } else if (KEEPALIVE_REPLY.equals(msg)) {
360 logger.trace("{} - Keepalive Reply", thingId);
361 } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
362 // 080a 12 0308cf08 180a
363 logger.debug("PIN Process Started");
364 } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
365 // This seems to be 20**** when observed. It is unclear what this does.
366 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
367 logger.trace("{} - Certificate To Follow", thingId);
368 } else if (msg.startsWith(MESSAGE_SUCCESS)) {
369 // Successful command received
370 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
371 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
373 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
374 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
375 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
376 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
377 callback.setLoggedIn(true);
378 } else if (TIMEOUT.equals(msg)) {
380 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
381 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
382 logger.debug("{} - Timeout {}", thingId, msg);
383 } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
384 // Get current app command successful. Usually paired with 0807 reply below.
385 logger.trace("{} - Current App Request Successful", thingId);
386 } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
388 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
390 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
391 StringBuilder appName = new StringBuilder();
392 String lengthStr = "" + charArray[34] + charArray[35];
393 int length = Integer.parseInt(lengthStr, 16) * 2;
394 for (int i = 36; i < 36 + length; i++) {
395 appName.append(charArray[i]);
397 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
398 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
399 } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
401 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
402 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
403 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
404 // |--------Little Endian Total Payload Length
405 // |-----------------------Little Endian Remaining Payload Length
406 // |-----------------------------------Length of SUCCESS
407 // |--------------------------------------ASCII: SUCCESS
408 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
409 // |------------------------------------------------------------Priv Key RSA 2048
410 // |--------------------------------------------------------------------Cert X.509
411 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
412 StringBuilder preamble = new StringBuilder();
413 StringBuilder privKey = new StringBuilder();
414 StringBuilder pubKey = new StringBuilder();
417 for (; i < 44; i++) {
418 preamble.append(charArray[i]);
420 logger.trace("{} - Cert Preamble: {}", thingId, preamble.toString());
423 String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
425 int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
429 logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
431 for (; i < current + privLen; i++) {
432 privKey.append(charArray[i]);
435 logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
437 for (; i < msg.length() - 4; i++) {
438 pubKey.append(charArray[i]);
441 logger.trace("{} - Cert pubKey: {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
443 logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
444 msg.length() - privLen - 4);
446 byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
447 byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
449 String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
450 String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
452 callback.setKeys(privKeyB64, pubKeyB64);
454 logger.info("{} - Pin Process Failed.", thingId);
457 logger.info("{} - Unknown payload received. {}", thingId, msg);
459 } catch (Exception e) {
460 logger.warn("{} - Message Parser Exception on {}", thingId, msg);
461 logger.warn("{} - Message Parser Caught Exception", thingId, e);