2 * Copyright (c) 2010-2023 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(DELIMITER_0A, 18)) {
208 // Individual update?
209 // 08f10712 5808061254 0a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 300118f107
210 logger.info("{} - Individual App Update - Please Report This: {}", thingId, msg);
211 } else if (msg.startsWith(MESSAGE_APPDB) && (msg.length() > 30)) {
212 // Massive dump of currently installed apps
213 // 08f10712 d81f080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
214 Map<String, String> appNameDB = new HashMap<>();
215 Map<String, String> appURLDB = new HashMap<>();
221 StringBuilder appSBPrepend = new StringBuilder();
222 StringBuilder appSBDN = new StringBuilder();
224 // Load default apps that don't get sent in payload
226 appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
227 appURLDB.put("com.google.android.tvlauncher", "");
229 appNameDB.put("com.google.android.katniss", "Google app for Android TV");
230 appURLDB.put("com.google.android.katniss", "");
232 appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
233 appURLDB.put("com.google.android.katnisspx", "");
235 appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
236 appURLDB.put("com.google.android.backdrop", "");
238 // Packet will end with 300118f107 after last entry
240 while (i < msg.length() - 10) {
241 StringBuilder appSBName = new StringBuilder();
242 StringBuilder appSBURL = new StringBuilder();
244 // There are instances such as plex where multiple apps are sent as part of the same payload
245 // This is identified when 12 is the beginning of the set
247 st = "" + charArray[i] + "" + charArray[i + 1];
249 if (!DELIMITER_12.equals(st)) {
250 appSBPrepend = new StringBuilder();
251 appSBDN = new StringBuilder();
256 // Usually 10 in length but can be longer or shorter so look for 0a twice
258 st = "" + charArray[i] + "" + charArray[i + 1];
259 appSBPrepend.append(st);
261 } while (!DELIMITER_0A.equals(st));
263 st = "" + charArray[i] + "" + charArray[i + 1];
264 appSBPrepend.append(st);
266 } while (!DELIMITER_0A.equals(st));
267 st = "" + charArray[i] + "" + charArray[i + 1];
269 // Look for a third 0a, but only if 12 is not down the line
270 // If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
271 String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
272 if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
273 appSBPrepend.append(st);
275 st = "" + charArray[i] + "" + charArray[i + 1];
279 length = Integer.parseInt(st, 16) * 2;
282 for (; i < current + length; i = i + 2) {
283 st = "" + charArray[i] + "" + charArray[i + 1];
287 logger.trace("Second Entry");
292 i += 2; // 12 delimiter
293 st = "" + charArray[i] + "" + charArray[i + 1];
295 length = Integer.parseInt(st, 16) * 2;
297 for (; i < current + length; i = i + 2) {
298 st = "" + charArray[i] + "" + charArray[i + 1];
299 appSBName.append(st);
302 // There are times where there is padding here for no reason beyond the specified length.
303 // Proceed forward until we get to the 22 delimiter
305 st = "" + charArray[i] + "" + charArray[i + 1];
306 while (!DELIMITER_22.equals(st)) {
308 st = "" + charArray[i] + "" + charArray[i + 1];
312 i += 2; // 22 delimiter
313 st = "" + charArray[i] + "" + charArray[i + 1];
315 length = Integer.parseInt(st, 16) * 2;
317 for (; i < current + length; i = i + 2) {
318 st = "" + charArray[i] + "" + charArray[i + 1];
321 st = "" + charArray[i] + "" + charArray[i + 1];
322 if (!DELIMITER_12.equals(st)) {
323 i += 4; // terminates 2801
325 String appPrepend = appSBPrepend.toString();
326 String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
327 String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
328 String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
329 logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
331 appNameDB.put(appDN, appName);
332 appURLDB.put(appDN, appURL);
335 Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
336 List<String> valueList = new ArrayList<>();
337 for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
338 valueList.add(entry.getValue());
340 Collections.sort(valueList);
341 for (String str : valueList) {
342 for (Entry<String, String> entry : appNameDB.entrySet()) {
343 if (entry.getValue().equals(str)) {
344 sortedAppNameDB.put(entry.getKey(), str);
349 logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
350 appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
351 callback.setAppDB(sortedAppNameDB, appURLDB);
353 logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
354 appNameDB.toString(), appURLDB.toString());
356 } else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
357 // This has something to do with successful command response, maybe.
358 logger.trace("{} - Good Command Response", thingId);
359 } else if (KEEPALIVE_REPLY.equals(msg)) {
361 logger.trace("{} - Keepalive Reply", thingId);
362 } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
363 // 080a 12 0308cf08 180a
364 logger.debug("PIN Process Started");
365 } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
366 // This seems to be 20**** when observed. It is unclear what this does.
367 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
368 logger.trace("{} - Certificate To Follow", thingId);
369 } else if (msg.startsWith(MESSAGE_SUCCESS)) {
370 // Successful command received
371 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
372 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
374 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
375 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
376 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
377 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
378 callback.setLoggedIn(true);
379 } else if (TIMEOUT.equals(msg)) {
381 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
382 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
383 logger.debug("{} - Timeout {}", thingId, msg);
384 } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
385 // Get current app command successful. Usually paired with 0807 reply below.
386 logger.trace("{} - Current App Request Successful", thingId);
387 } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
389 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
391 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
392 StringBuilder appName = new StringBuilder();
393 String lengthStr = "" + charArray[34] + charArray[35];
394 int length = Integer.parseInt(lengthStr, 16) * 2;
395 for (int i = 36; i < 36 + length; i++) {
396 appName.append(charArray[i]);
398 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
399 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
400 } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
402 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
403 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
404 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
405 // |--------Little Endian Total Payload Length
406 // |-----------------------Little Endian Remaining Payload Length
407 // |-----------------------------------Length of SUCCESS
408 // |--------------------------------------ASCII: SUCCESS
409 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
410 // |------------------------------------------------------------Priv Key RSA 2048
411 // |--------------------------------------------------------------------Cert X.509
412 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
413 StringBuilder preamble = new StringBuilder();
414 StringBuilder privKey = new StringBuilder();
415 StringBuilder pubKey = new StringBuilder();
418 for (; i < 44; i++) {
419 preamble.append(charArray[i]);
421 logger.trace("{} - Cert Preamble: {}", thingId, preamble.toString());
424 String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
426 int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
430 logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
432 for (; i < current + privLen; i++) {
433 privKey.append(charArray[i]);
436 logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
438 for (; i < msg.length() - 4; i++) {
439 pubKey.append(charArray[i]);
442 logger.trace("{} - Cert pubKey: {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
444 logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
445 msg.length() - privLen - 4);
447 byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
448 byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
450 String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
451 String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
453 callback.setKeys(privKeyB64, pubKeyB64);
455 logger.info("{} - Pin Process Failed.", thingId);
458 logger.info("{} - Unknown payload received. {}", thingId, msg);
460 } catch (Exception e) {
461 logger.warn("{} - Message Parser Exception on {}", thingId, msg);
462 logger.warn("{} - Message Parser Caught Exception", thingId, e);