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 Padding? 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);
109 String st = "" + charArray[i] + "" + charArray[i + 1];
110 length = Integer.parseInt(st, 16) * 2;
113 StringBuilder hostname = new StringBuilder();
116 for (; i < current + length; i = i + 2) {
117 st = "" + charArray[i] + "" + charArray[i + 1];
124 st = "" + charArray[i] + "" + charArray[i + 1];
125 length = Integer.parseInt(st, 16) * 2;
128 StringBuilder ipAddress = new StringBuilder();
131 for (; i < current + length; i = i + 2) {
132 st = "" + charArray[i] + "" + charArray[i + 1];
133 ipAddress.append(st);
136 st = "" + charArray[i] + "" + charArray[i + 1];
137 while (!DELIMITER_22.equals(st)) {
139 st = "" + charArray[i] + "" + charArray[i + 1];
146 st = "" + charArray[i] + "" + charArray[i + 1];
147 length = Integer.parseInt(st, 16) * 2;
150 StringBuilder deviceId = new StringBuilder();
153 for (; i < current + length; i = i + 2) {
154 st = "" + charArray[i] + "" + charArray[i + 1];
159 st = "" + charArray[i] + "" + charArray[i + 1];
160 StringBuilder arch = new StringBuilder();
161 while (DELIMITER_2A.equals(st)) {
163 st = "" + charArray[i] + "" + charArray[i + 1];
164 length = Integer.parseInt(st, 16) * 2;
167 for (; i < current + length; i = i + 2) {
168 st = "" + charArray[i] + "" + charArray[i + 1];
171 st = "" + charArray[i] + "" + charArray[i + 1];
172 if (DELIMITER_2A.equals(st)) {
177 String encHostname = ShieldTVRequest.encodeMessage(hostname.toString());
178 String encIpAddress = ShieldTVRequest.encodeMessage(ipAddress.toString());
179 String encDeviceId = ShieldTVRequest.encodeMessage(deviceId.toString());
180 String encArch = ShieldTVRequest.encodeMessage(arch.toString());
181 logger.debug("{} - Hostname: {} - ipAddress: {} - deviceId: {} - arch: {}", thingId, encHostname,
182 encIpAddress, encDeviceId, encArch);
183 callback.setHostName(encHostname);
184 callback.setDeviceID(encDeviceId);
185 callback.setArch(encArch);
186 } else if (APP_START_SUCCESS.equals(msg)) {
187 // App successfully started
188 logger.debug("{} - App started successfully", thingId);
189 } else if (APP_START_FAILED.equals(msg)) {
190 // App failed to start
191 logger.debug("{} - App failed to start", thingId);
192 } else if (msg.startsWith(MESSAGE_APPDB) && msg.startsWith(DELIMITER_0A, 18)) {
193 // Individual update?
194 // 08f10712 5808061254 0a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 300118f107
195 logger.info("{} - Individual App Update - Please Report This: {}", thingId, msg);
196 } else if (msg.startsWith(MESSAGE_APPDB) && (msg.length() > 30)) {
197 // Massive dump of currently installed apps
198 // 08f10712 d81f080112 d31f0a540a LEN app.name 12 LEN app.real.name 22 LEN URL 2801 30010a650a LEN
199 Map<String, String> appNameDB = new HashMap<>();
200 Map<String, String> appURLDB = new HashMap<>();
206 StringBuilder appSBPrepend = new StringBuilder();
207 StringBuilder appSBDN = new StringBuilder();
209 // Load default apps that don't get sent in payload
211 appNameDB.put("com.google.android.tvlauncher", "Android TV Home");
212 appURLDB.put("com.google.android.tvlauncher", "");
214 appNameDB.put("com.google.android.katniss", "Google app for Android TV");
215 appURLDB.put("com.google.android.katniss", "");
217 appNameDB.put("com.google.android.katnisspx", "Google app for Android TV (Pictures)");
218 appURLDB.put("com.google.android.katnisspx", "");
220 appNameDB.put("com.google.android.backdrop", "Backdrop Daydream");
221 appURLDB.put("com.google.android.backdrop", "");
223 // Packet will end with 300118f107 after last entry
225 while (i < msg.length() - 10) {
226 StringBuilder appSBName = new StringBuilder();
227 StringBuilder appSBURL = new StringBuilder();
229 // There are instances such as plex where multiple apps are sent as part of the same payload
230 // This is identified when 12 is the beginning of the set
232 st = "" + charArray[i] + "" + charArray[i + 1];
234 if (!DELIMITER_12.equals(st)) {
235 appSBPrepend = new StringBuilder();
236 appSBDN = new StringBuilder();
241 // Usually 10 in length but can be longer or shorter so look for 0a twice
243 st = "" + charArray[i] + "" + charArray[i + 1];
244 appSBPrepend.append(st);
246 } while (!DELIMITER_0A.equals(st));
248 st = "" + charArray[i] + "" + charArray[i + 1];
249 appSBPrepend.append(st);
251 } while (!DELIMITER_0A.equals(st));
252 st = "" + charArray[i] + "" + charArray[i + 1];
254 // Look for a third 0a, but only if 12 is not down the line
255 // If 12 is exactly 20 away from 0a that means that the DN was actually 10 long
256 String st2 = "" + charArray[i + 22] + "" + charArray[i + 23];
257 if (DELIMITER_0A.equals(st.toString()) && !DELIMITER_12.equals(st2)) {
258 appSBPrepend.append(st);
260 st = "" + charArray[i] + "" + charArray[i + 1];
264 length = Integer.parseInt(st, 16) * 2;
267 for (; i < current + length; i = i + 2) {
268 st = "" + charArray[i] + "" + charArray[i + 1];
272 logger.trace("Second Entry");
277 i += 2; // 12 delimiter
278 st = "" + charArray[i] + "" + charArray[i + 1];
280 length = Integer.parseInt(st, 16) * 2;
282 for (; i < current + length; i = i + 2) {
283 st = "" + charArray[i] + "" + charArray[i + 1];
284 appSBName.append(st);
287 // There are times where there is padding here for no reason beyond the specified length.
288 // Proceed forward until we get to the 22 delimiter
290 st = "" + charArray[i] + "" + charArray[i + 1];
291 while (!DELIMITER_22.equals(st)) {
293 st = "" + charArray[i] + "" + charArray[i + 1];
297 i += 2; // 22 delimiter
298 st = "" + charArray[i] + "" + charArray[i + 1];
300 length = Integer.parseInt(st, 16) * 2;
302 for (; i < current + length; i = i + 2) {
303 st = "" + charArray[i] + "" + charArray[i + 1];
306 st = "" + charArray[i] + "" + charArray[i + 1];
307 if (!DELIMITER_12.equals(st)) {
308 i += 4; // terminates 2801
310 String appPrepend = appSBPrepend.toString();
311 String appDN = ShieldTVRequest.encodeMessage(appSBDN.toString());
312 String appName = ShieldTVRequest.encodeMessage(appSBName.toString());
313 String appURL = ShieldTVRequest.encodeMessage(appSBURL.toString());
314 logger.debug("{} - AppPrepend: {} AppDN: {} AppName: {} AppURL: {}", thingId, appPrepend, appDN,
316 appNameDB.put(appDN, appName);
317 appURLDB.put(appDN, appURL);
320 Map<String, String> sortedAppNameDB = new LinkedHashMap<>();
321 List<String> valueList = new ArrayList<>();
322 for (Map.Entry<String, String> entry : appNameDB.entrySet()) {
323 valueList.add(entry.getValue());
325 Collections.sort(valueList);
326 for (String str : valueList) {
327 for (Entry<String, String> entry : appNameDB.entrySet()) {
328 if (entry.getValue().equals(str)) {
329 sortedAppNameDB.put(entry.getKey(), str);
334 logger.trace("{} - MP appNameDB: {} sortedAppNameDB: {} appURLDB: {}", thingId,
335 appNameDB.toString(), sortedAppNameDB.toString(), appURLDB.toString());
336 callback.setAppDB(sortedAppNameDB, appURLDB);
338 logger.warn("{} - MP empty msg: {} appDB appNameDB: {} appURLDB: {}", thingId, msg,
339 appNameDB.toString(), appURLDB.toString());
341 } else if (msg.startsWith(MESSAGE_GOOD_COMMAND)) {
342 // This has something to do with successful command response, maybe.
343 } else if (KEEPALIVE_REPLY.equals(msg)) {
345 } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_PINSTART, 6)) {
346 // 080a 12 0308cf08 180a
347 logger.debug("PIN Process Started");
348 } else if (msg.startsWith(MESSAGE_CERT_COMING) && msg.length() == 6) {
349 // This seems to be 20**** when observed. It is unclear what this does.
350 // This seems to send immediately before the certificate reply and as a reply to the pin being sent
351 } else if (msg.startsWith(MESSAGE_SUCCESS)) {
352 // Successful command received
353 // 08f007 12 0c 0804 12 08 0a0608 01100c200f 18f007 - GOOD LOGIN
354 // 08f007 12 LEN 0804 12 LEN 0a0608 01100c200f 18f007
356 // 08f00712 0c 0804 12 08 0a0608 01100e200f 18f007 KEY_VOLDOWN
357 // 08f00712 0c 0804 12 08 0a0608 01100f200f 18f007 KEY_VOLUP
358 // 08f00712 0c 0804 12 08 0a0608 01200f2801 18f007 KEY_MUTE
359 logger.info("{} - Login Successful to {}", thingId, callback.getHostName());
360 callback.setLoggedIn(true);
361 } else if (TIMEOUT.equals(msg)) {
363 // 080a 12 1108b510 12 0c0804 12 08 54696d65206f7574 180a
364 // 080a 12 1108b510 12 0c0804 12 LEN Timeout 180a
365 logger.debug("{} - Timeout {}", thingId, msg);
366 } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_GET_SUCCESS, 10)) {
367 // Get current app command successful. Usually paired with 0807 reply below.
368 } else if (msg.startsWith(MESSAGE_APP_SUCCESS) && msg.startsWith(MESSAGE_APP_CURRENT, 10)) {
370 // 08ec07 12 2a0807 22 262205 656e5f555342 1d 636f6d2e676f6f676c652e616e64726f69642e74766c61756e63686572
372 // 08ec07 12 2a0807 22 262205 en_USB LEN AppName 18ec07
373 StringBuilder appName = new StringBuilder();
374 String lengthStr = "" + charArray[34] + charArray[35];
375 int length = Integer.parseInt(lengthStr, 16) * 2;
376 for (int i = 36; i < 36 + length; i++) {
377 appName.append(charArray[i]);
379 logger.debug("{} - Current App: {}", thingId, ShieldTVRequest.encodeMessage(appName.toString()));
380 callback.setCurrentApp(ShieldTVRequest.encodeMessage(appName.toString()));
381 } else if (msg.startsWith(MESSAGE_LOWPRIV) && msg.startsWith(MESSAGE_CERT, 10)) {
383 // |--6-----------12-----------10---------------16---------6--- = 50 characters long
384 // |080a 12 ad10 08b510 12 a710 0801 12 07 53756363657373 1ac009 3082... 3082... 180a
385 // |080a 12 9f10 08b510 12 9910 0801 12 07 53756363657373 1ac209 3082... 3082... 180a
386 // |--------Little Endian Total Payload Length
387 // |-----------------------Little Endian Remaining Payload Length
388 // |-----------------------------------Length of SUCCESS
389 // |--------------------------------------ASCII: SUCCESS
390 // |-----------------------------------------------------Little Endian Length (e.g. 09c0 and 09c2 above)
391 // |------------------------------------------------------------Priv Key RSA 2048
392 // |--------------------------------------------------------------------Cert X.509
393 if (msg.startsWith(MESSAGE_CERT_PAYLOAD, 28)) {
394 StringBuilder preamble = new StringBuilder();
395 StringBuilder privKey = new StringBuilder();
396 StringBuilder pubKey = new StringBuilder();
399 for (; i < 44; i++) {
400 preamble.append(charArray[i]);
402 logger.trace("{} - Cert Preamble: {}", thingId, preamble.toString());
405 String st = "" + charArray[i + 2] + "" + charArray[i + 3] + "" + charArray[i] + ""
407 int privLen = 2246 + ((Integer.parseInt(st, 16) - 2400) * 2);
411 logger.trace("{} - Cert privLen: {} {}", thingId, st, privLen);
413 for (; i < current + privLen; i++) {
414 privKey.append(charArray[i]);
417 logger.trace("{} - Cert privKey: {} {}", thingId, privLen, privKey.toString());
419 for (; i < msg.length() - 4; i++) {
420 pubKey.append(charArray[i]);
423 logger.trace("{} - Cert pubKey: {} {}", thingId, msg.length() - privLen - 4, pubKey.toString());
425 logger.debug("{} - Cert Pair Received privLen: {} pubLen: {}", thingId, privLen,
426 msg.length() - privLen - 4);
428 byte[] privKeyB64Byte = DatatypeConverter.parseHexBinary(privKey.toString());
429 byte[] pubKeyB64Byte = DatatypeConverter.parseHexBinary(pubKey.toString());
431 String privKeyB64 = Base64.getEncoder().encodeToString(privKeyB64Byte);
432 String pubKeyB64 = Base64.getEncoder().encodeToString(pubKeyB64Byte);
434 callback.setKeys(privKeyB64, pubKeyB64);
436 logger.info("{} - Pin Process Failed.", thingId);
439 logger.info("{} - Unknown payload received. {}", thingId, msg);
441 } catch (Exception e) {
442 logger.info("{} - Message Parser Caught Exception", thingId, e);