]> git.basschouten.com Git - openhab-addons.git/blob
66e505db80768d26ab44f65c3d9ef3179c425b71
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.androidtv.internal.protocol.shieldtv;
14
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.shieldtv.ShieldTVConstants.*;
17
18 import java.io.BufferedReader;
19 import java.io.BufferedWriter;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.InterruptedIOException;
24 import java.io.OutputStreamWriter;
25 import java.math.BigInteger;
26 import java.net.ConnectException;
27 import java.net.InetSocketAddress;
28 import java.net.NoRouteToHostException;
29 import java.net.ServerSocket;
30 import java.net.Socket;
31 import java.net.SocketAddress;
32 import java.net.SocketTimeoutException;
33 import java.net.UnknownHostException;
34 import java.nio.charset.StandardCharsets;
35 import java.security.GeneralSecurityException;
36 import java.security.NoSuchAlgorithmException;
37 import java.security.UnrecoverableKeyException;
38 import java.security.cert.Certificate;
39 import java.security.cert.X509Certificate;
40 import java.util.HashMap;
41 import java.util.Map;
42 import java.util.concurrent.BlockingQueue;
43 import java.util.concurrent.Future;
44 import java.util.concurrent.LinkedBlockingQueue;
45 import java.util.concurrent.ScheduledExecutorService;
46 import java.util.concurrent.ScheduledFuture;
47 import java.util.concurrent.TimeUnit;
48
49 import javax.net.ssl.KeyManagerFactory;
50 import javax.net.ssl.SSLContext;
51 import javax.net.ssl.SSLServerSocketFactory;
52 import javax.net.ssl.SSLSession;
53 import javax.net.ssl.SSLSocket;
54 import javax.net.ssl.SSLSocketFactory;
55 import javax.net.ssl.TrustManager;
56 import javax.net.ssl.X509TrustManager;
57
58 import org.eclipse.jdt.annotation.NonNullByDefault;
59 import org.eclipse.jdt.annotation.Nullable;
60 import org.openhab.binding.androidtv.internal.AndroidTVHandler;
61 import org.openhab.binding.androidtv.internal.AndroidTVTranslationProvider;
62 import org.openhab.binding.androidtv.internal.utils.AndroidTVPKI;
63 import org.openhab.core.OpenHAB;
64 import org.openhab.core.library.types.StringType;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.types.Command;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 /**
71  * The {@link ShieldTVConnectionManager} is responsible for handling connections via the shieldtv protocol
72  *
73  * Significant portions reused from Lutron binding with permission from Bob A.
74  *
75  * @author Ben Rosenblum - Initial contribution
76  */
77 @NonNullByDefault
78 public class ShieldTVConnectionManager {
79     private static final int DEFAULT_RECONNECT_SECONDS = 60;
80     private static final int DEFAULT_HEARTBEAT_SECONDS = 5;
81     private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
82     private static final String DEFAULT_KEYSTORE_PASSWORD = "secret";
83     private static final int DEFAULT_PORT = 8987;
84
85     private final Logger logger = LoggerFactory.getLogger(ShieldTVConnectionManager.class);
86
87     private ScheduledExecutorService scheduler;
88
89     private final AndroidTVHandler handler;
90     private ShieldTVConfiguration config;
91     private final AndroidTVTranslationProvider translationProvider;
92
93     private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
94     private @Nullable SSLSocket sslSocket;
95     private @Nullable BufferedWriter writer;
96     private @Nullable BufferedReader reader;
97
98     private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
99     private @Nullable Socket shimServerSocket;
100     private @Nullable BufferedWriter shimWriter;
101     private @Nullable BufferedReader shimReader;
102
103     private @NonNullByDefault({}) ShieldTVMessageParser messageParser;
104
105     private final BlockingQueue<ShieldTVCommand> sendQueue = new LinkedBlockingQueue<>();
106     private final BlockingQueue<ShieldTVCommand> shimQueue = new LinkedBlockingQueue<>();
107
108     private @Nullable Future<?> asyncInitializeTask;
109     private @Nullable Future<?> shimAsyncInitializeTask;
110
111     private @Nullable Thread senderThread;
112     private @Nullable Thread readerThread;
113     private @Nullable Thread shimSenderThread;
114     private @Nullable Thread shimReaderThread;
115
116     private @Nullable ScheduledFuture<?> keepAliveJob;
117     private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
118     private @Nullable ScheduledFuture<?> connectRetryJob;
119     private final Object keepAliveReconnectLock = new Object();
120     private final Object connectionLock = new Object();
121     private int periodicUpdate;
122
123     private @Nullable ScheduledFuture<?> deviceHealthJob;
124     private boolean isOnline = true;
125
126     private StringBuffer sbReader = new StringBuffer();
127     private StringBuffer sbShimReader = new StringBuffer();
128     private String lastMsg = "";
129     private String thisMsg = "";
130     private boolean inMessage = false;
131     private String msgType = "";
132
133     private boolean disposing = false;
134     private boolean isLoggedIn = false;
135     private String statusMessage = "";
136
137     private String hostName = "";
138     private String currentApp = "";
139     private String deviceId = "";
140     private String arch = "";
141
142     private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
143     private byte[] encryptionKey;
144
145     private boolean appDBPopulated = false;
146     private Map<String, String> appNameDB = new HashMap<>();
147     private Map<String, String> appURLDB = new HashMap<>();
148
149     public ShieldTVConnectionManager(AndroidTVHandler handler, ShieldTVConfiguration config) {
150         messageParser = new ShieldTVMessageParser(this);
151         this.config = config;
152         this.handler = handler;
153         this.translationProvider = handler.getTranslationProvider();
154         this.scheduler = handler.getScheduler();
155         this.encryptionKey = androidtvPKI.generateEncryptionKey();
156         initialize();
157     }
158
159     public void setHostName(String hostName) {
160         this.hostName = hostName;
161         handler.setThingProperty("deviceName", hostName);
162     }
163
164     public String getHostName() {
165         return hostName;
166     }
167
168     public String getThingID() {
169         return handler.getThingID();
170     }
171
172     public void setDeviceID(String deviceId) {
173         this.deviceId = deviceId;
174         handler.setThingProperty("deviceID", deviceId);
175     }
176
177     public String getDeviceID() {
178         return deviceId;
179     }
180
181     public void setArch(String arch) {
182         this.arch = arch;
183         handler.setThingProperty("architectures", arch);
184     }
185
186     public String getArch() {
187         return arch;
188     }
189
190     public void setCurrentApp(String currentApp) {
191         this.currentApp = currentApp;
192         handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
193
194         if (this.appDBPopulated) {
195             String appName = "";
196             String appURL = "";
197
198             if (appNameDB.get(currentApp) != null) {
199                 appName = appNameDB.get(currentApp);
200                 handler.updateChannelState(CHANNEL_APPNAME, new StringType(appName));
201             } else {
202                 logger.info("Unknown Android App: {}", currentApp);
203                 handler.updateChannelState(CHANNEL_APPNAME, new StringType(""));
204             }
205
206             if (appURLDB.get(currentApp) != null) {
207                 appURL = appURLDB.get(currentApp);
208                 handler.updateChannelState(CHANNEL_APPURL, new StringType(appURL));
209             } else {
210                 handler.updateChannelState(CHANNEL_APPURL, new StringType(""));
211             }
212         }
213     }
214
215     public String getStatusMessage() {
216         return statusMessage;
217     }
218
219     private void setStatus(boolean isLoggedIn) {
220         if (isLoggedIn) {
221             setStatus(isLoggedIn, "online.online");
222         } else {
223             setStatus(isLoggedIn, "offline.unknown");
224         }
225     }
226
227     private void setStatus(boolean isLoggedIn, String statusMessage) {
228         String translatedMessage = translationProvider.getText(statusMessage);
229         if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(translatedMessage))) {
230             this.isLoggedIn = isLoggedIn;
231             this.statusMessage = translatedMessage;
232             handler.checkThingStatus();
233         }
234     }
235
236     public String getCurrentApp() {
237         return currentApp;
238     }
239
240     private void sendPeriodicUpdate() {
241         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("080b120308cd08"))); // Get Hostname
242         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08f30712020805"))); // No Reply
243         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08f10712020800"))); // Get App DB
244         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
245     }
246
247     public void setLoggedIn(boolean isLoggedIn) {
248         if (!this.isLoggedIn && isLoggedIn) {
249             sendPeriodicUpdate();
250         }
251
252         if (this.isLoggedIn != isLoggedIn) {
253             setStatus(isLoggedIn);
254         }
255     }
256
257     public boolean getLoggedIn() {
258         return isLoggedIn;
259     }
260
261     private boolean servicePing() {
262         int timeout = 500;
263
264         SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
265         try (Socket socket = new Socket()) {
266             socket.connect(socketAddress, timeout);
267             return true;
268         } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
269             return false;
270         } catch (IOException ignored) {
271             // IOException is thrown by automatic close() of the socket.
272             // This should actually never return a value as we should return true above already
273             return true;
274         }
275     }
276
277     private void checkHealth() {
278         boolean isOnline;
279         if (!isLoggedIn) {
280             isOnline = servicePing();
281         } else {
282             isOnline = true;
283         }
284         logger.debug("{} - Device Health - Online: {} - Logged In: {}", handler.getThingID(), isOnline, isLoggedIn);
285         if (isOnline != this.isOnline) {
286             this.isOnline = isOnline;
287             if (isOnline) {
288                 logger.debug("{} - Device is back online.  Attempting reconnection.", handler.getThingID());
289                 reconnect();
290             }
291         }
292     }
293
294     public void setKeys(String privKey, String cert) {
295         try {
296             androidtvPKI.setKeys(privKey, encryptionKey, cert);
297             androidtvPKI.saveKeyStore(config.keystorePassword, encryptionKey);
298         } catch (GeneralSecurityException e) {
299             logger.debug("General security exception", e);
300         } catch (IOException e) {
301             logger.debug("IO Exception", e);
302         } catch (Exception e) {
303             logger.debug("General Exception", e);
304         }
305     }
306
307     public void setAppDB(Map<String, String> appNameDB, Map<String, String> appURLDB) {
308         this.appNameDB = appNameDB;
309         this.appURLDB = appURLDB;
310         this.appDBPopulated = true;
311         logger.debug("{} - App DB Populated", handler.getThingID());
312         logger.trace("{} - Handler appNameDB: {} appURLDB: {}", handler.getThingID(), this.appNameDB, this.appURLDB);
313         handler.updateCDP(CHANNEL_APP, this.appNameDB);
314     }
315
316     private TrustManager[] defineNoOpTrustManager() {
317         return new TrustManager[] { new X509TrustManager() {
318             @Override
319             public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
320                 logger.debug("Assuming client certificate is valid");
321                 if (chain != null && logger.isTraceEnabled()) {
322                     for (int cert = 0; cert < chain.length; cert++) {
323                         logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
324                         logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
325                         logger.trace("Serial number: {}", chain[cert].getSerialNumber());
326                     }
327                 }
328             }
329
330             @Override
331             public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
332                 logger.debug("Assuming server certificate is valid");
333                 if (chain != null && logger.isTraceEnabled()) {
334                     for (int cert = 0; cert < chain.length; cert++) {
335                         logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
336                         logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
337                         logger.trace("Serial number: {}", chain[cert].getSerialNumber());
338                     }
339                 }
340             }
341
342             @Override
343             public X509Certificate @Nullable [] getAcceptedIssuers() {
344                 return null;
345             }
346         } };
347     }
348
349     private void initialize() {
350         SSLContext sslContext;
351
352         String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
353         File folder = new File(folderName);
354
355         if (!folder.exists()) {
356             logger.debug("Creating directory {}", folderName);
357             folder.mkdirs();
358         }
359
360         config.port = (config.port > 0) ? config.port : DEFAULT_PORT;
361
362         config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
363                 : folderName + "/shieldtv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
364                         + ".keystore";
365         config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
366                 : DEFAULT_KEYSTORE_PASSWORD;
367
368         androidtvPKI.setKeystoreFileName(config.keystoreFileName);
369         androidtvPKI.setAlias("nvidia");
370
371         deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
372                 TimeUnit.SECONDS);
373
374         try {
375             File keystoreFile = new File(config.keystoreFileName);
376
377             if (!keystoreFile.exists() || config.shimNewKeys) {
378                 androidtvPKI.generateNewKeyPair(encryptionKey);
379                 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
380             } else {
381                 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
382             }
383
384             logger.trace("{} - Initializing SSL Context", handler.getThingID());
385             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
386             kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
387                     config.keystorePassword.toCharArray());
388
389             TrustManager[] trustManagers = defineNoOpTrustManager();
390
391             sslContext = SSLContext.getInstance("TLS");
392             sslContext.init(kmf.getKeyManagers(), trustManagers, null);
393
394             sslSocketFactory = sslContext.getSocketFactory();
395             if (!config.shim) {
396                 asyncInitializeTask = scheduler.submit(this::connect);
397             } else {
398                 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
399             }
400         } catch (NoSuchAlgorithmException | IOException e) {
401             setStatus(false, "offline.error-initalizing-keystore");
402             logger.debug("Error initializing keystore", e);
403         } catch (UnrecoverableKeyException e) {
404             setStatus(false, "offline.key-unrecoverable-with-supplied-password");
405         } catch (GeneralSecurityException e) {
406             logger.debug("General security exception", e);
407         } catch (Exception e) {
408             logger.debug("General exception", e);
409         }
410     }
411
412     public void connect() {
413         synchronized (connectionLock) {
414             if (isOnline) {
415                 try {
416                     logger.debug("{} - Opening ShieldTV SSL connection to {}:{}", handler.getThingID(),
417                             config.ipAddress, config.port);
418                     SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
419                     sslSocket.startHandshake();
420                     writer = new BufferedWriter(
421                             new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
422                     reader = new BufferedReader(
423                             new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
424                     this.sslSocket = sslSocket;
425                 } catch (UnknownHostException e) {
426                     setStatus(false, "offline.unknown-host");
427                     return;
428                 } catch (IllegalArgumentException e) {
429                     // port out of valid range
430                     setStatus(false, "offline.invalid-port-number");
431                     return;
432                 } catch (InterruptedIOException e) {
433                     logger.debug("Interrupted while establishing ShieldTV connection");
434                     Thread.currentThread().interrupt();
435                     return;
436                 } catch (IOException e) {
437                     setStatus(false, "offline.error-opening-ssl-connection-check-log");
438                     logger.info("{} - Error opening SSL connection to {}:{} {}", handler.getThingID(), config.ipAddress,
439                             config.port, e.getMessage());
440                     disconnect(false);
441                     scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
442                     return;
443                 }
444
445                 setStatus(false, "offline.initializing");
446
447                 Thread readerThread = new Thread(this::readerThreadJob, "ShieldTV reader " + handler.getThingID());
448                 readerThread.setDaemon(true);
449                 readerThread.start();
450                 this.readerThread = readerThread;
451
452                 Thread senderThread = new Thread(this::senderThreadJob, "ShieldTV sender " + handler.getThingID());
453                 senderThread.setDaemon(true);
454                 senderThread.start();
455                 this.senderThread = senderThread;
456
457                 if (!config.shim) {
458                     this.periodicUpdate = 20;
459                     logger.debug("{} - Starting ShieldTV keepalive job with interval {}", handler.getThingID(),
460                             config.heartbeat);
461                     keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, config.heartbeat,
462                             config.heartbeat, TimeUnit.SECONDS);
463
464                     String login = ShieldTVRequest.encodeMessage(ShieldTVRequest.loginRequest());
465                     sendCommand(new ShieldTVCommand(login));
466                 }
467             } else {
468                 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
469             }
470         }
471     }
472
473     public void shimInitialize() {
474         synchronized (connectionLock) {
475             AndroidTVPKI shimPKI = new AndroidTVPKI();
476             byte[] shimEncryptionKey = shimPKI.generateEncryptionKey();
477             SSLContext sslContext;
478
479             try {
480                 shimPKI.generateNewKeyPair(shimEncryptionKey);
481                 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
482                 kmf.init(shimPKI.getKeyStore(config.keystorePassword, shimEncryptionKey),
483                         config.keystorePassword.toCharArray());
484                 TrustManager[] trustManagers = defineNoOpTrustManager();
485                 sslContext = SSLContext.getInstance("TLS");
486                 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
487                 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
488
489                 logger.debug("{} - Opening ShieldTV shim on port {}", handler.getThingID(), config.port);
490                 ServerSocket sslServerSocket = this.sslServerSocketFactory.createServerSocket(config.port);
491
492                 while (true) {
493                     logger.debug("{} - Waiting for shim connection...", handler.getThingID());
494                     Socket serverSocket = sslServerSocket.accept();
495                     disconnect(false);
496                     connect();
497                     SSLSession session = ((SSLSocket) serverSocket).getSession();
498                     Certificate[] cchain2 = session.getLocalCertificates();
499                     for (int i = 0; i < cchain2.length; i++) {
500                         logger.trace("Connection from: {}", ((X509Certificate) cchain2[i]).getSubjectX500Principal());
501                     }
502
503                     logger.trace("Peer host is {}", session.getPeerHost());
504                     logger.trace("Cipher is {}", session.getCipherSuite());
505                     logger.trace("Protocol is {}", session.getProtocol());
506                     logger.trace("ID is {}", new BigInteger(session.getId()));
507                     logger.trace("Session created in {}", session.getCreationTime());
508                     logger.trace("Session accessed in {}", session.getLastAccessedTime());
509
510                     shimWriter = new BufferedWriter(
511                             new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
512                     shimReader = new BufferedReader(
513                             new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
514                     this.shimServerSocket = serverSocket;
515
516                     Thread readerThread = new Thread(this::shimReaderThreadJob,
517                             "ShieldTV shim reader " + handler.getThingID());
518                     readerThread.setDaemon(true);
519                     readerThread.start();
520                     this.shimReaderThread = readerThread;
521
522                     Thread senderThread = new Thread(this::shimSenderThreadJob,
523                             "ShieldTV shim sender" + handler.getThingID());
524                     senderThread.setDaemon(true);
525                     senderThread.start();
526                     this.shimSenderThread = senderThread;
527                 }
528             } catch (Exception e) {
529                 logger.trace("Shim initalization exception", e);
530                 return;
531             }
532         }
533     }
534
535     private void scheduleConnectRetry(long waitSeconds) {
536         logger.trace("{} - Scheduling ShieldTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
537         connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
538     }
539
540     /**
541      * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
542      * clean up.
543      *
544      * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
545      *            connect or reconnect, and true when calling from dispose.
546      */
547     private void disconnect(boolean interruptAll) {
548         synchronized (connectionLock) {
549             logger.debug("{} - Disconnecting ShieldTV", handler.getThingID());
550
551             this.isLoggedIn = false;
552
553             ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
554             if (connectRetryJob != null) {
555                 connectRetryJob.cancel(true);
556             }
557             ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
558             if (keepAliveJob != null) {
559                 keepAliveJob.cancel(true);
560             }
561
562             reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
563
564             Thread senderThread = this.senderThread;
565             if (senderThread != null && senderThread.isAlive()) {
566                 senderThread.interrupt();
567             }
568
569             Thread readerThread = this.readerThread;
570             if (readerThread != null && readerThread.isAlive()) {
571                 readerThread.interrupt();
572             }
573
574             Thread shimSenderThread = this.shimSenderThread;
575             if (shimSenderThread != null && shimSenderThread.isAlive()) {
576                 shimSenderThread.interrupt();
577             }
578
579             Thread shimReaderThread = this.shimReaderThread;
580             if (shimReaderThread != null && shimReaderThread.isAlive()) {
581                 shimReaderThread.interrupt();
582             }
583
584             SSLSocket sslSocket = this.sslSocket;
585             if (sslSocket != null) {
586                 try {
587                     sslSocket.close();
588                 } catch (IOException e) {
589                     logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
590                 }
591                 this.sslSocket = null;
592             }
593             BufferedReader reader = this.reader;
594             if (reader != null) {
595                 try {
596                     reader.close();
597                 } catch (IOException e) {
598                     logger.debug("Error closing reader: {}", e.getMessage());
599                 }
600             }
601             BufferedWriter writer = this.writer;
602             if (writer != null) {
603                 try {
604                     writer.close();
605                 } catch (IOException e) {
606                     logger.debug("Error closing writer: {}", e.getMessage());
607                 }
608             }
609
610             Socket shimServerSocket = this.shimServerSocket;
611             if (shimServerSocket != null) {
612                 try {
613                     shimServerSocket.close();
614                 } catch (IOException e) {
615                     logger.debug("Error closing ShieldTV SSL socket: {}", e.getMessage());
616                 }
617                 this.shimServerSocket = null;
618             }
619             BufferedReader shimReader = this.shimReader;
620             if (shimReader != null) {
621                 try {
622                     shimReader.close();
623                 } catch (IOException e) {
624                     logger.debug("Error closing shimReader: {}", e.getMessage());
625                 }
626             }
627             BufferedWriter shimWriter = this.shimWriter;
628             if (shimWriter != null) {
629                 try {
630                     shimWriter.close();
631                 } catch (IOException e) {
632                     logger.debug("Error closing shimWriter: {}", e.getMessage());
633                 }
634             }
635         }
636     }
637
638     private void reconnect() {
639         synchronized (connectionLock) {
640             if (!this.disposing) {
641                 logger.debug("{} - Attempting to reconnect to the ShieldTV", handler.getThingID());
642                 setStatus(false, "offline.reconnecting");
643                 disconnect(false);
644                 connect();
645             }
646         }
647     }
648
649     /**
650      * Method executed by the message sender thread (senderThread)
651      */
652     private void senderThreadJob() {
653         logger.debug("{} - Command sender thread started", handler.getThingID());
654         try {
655             while (!Thread.currentThread().isInterrupted() && writer != null) {
656                 ShieldTVCommand command = sendQueue.take();
657
658                 try {
659                     BufferedWriter writer = this.writer;
660                     if (writer != null) {
661                         logger.trace("{} - Raw ShieldTV command decodes as: {}", handler.getThingID(),
662                                 ShieldTVRequest.decodeMessage(command.toString()));
663                         writer.write(command.toString());
664                         writer.flush();
665                     }
666                 } catch (InterruptedIOException e) {
667                     logger.debug("Interrupted while sending to ShieldTV");
668                     setStatus(false, "offline.interrupted");
669                     break; // exit loop and terminate thread
670                 } catch (IOException e) {
671                     logger.warn("{} - Communication error, will try to reconnect ShieldTV. Error: {}",
672                             handler.getThingID(), e.getMessage());
673                     setStatus(false, "offline.communication-error-will-try-to-reconnect");
674                     sendQueue.add(command); // Requeue command
675                     this.isLoggedIn = false;
676                     reconnect();
677                     break; // reconnect() will start a new thread; terminate this one
678                 }
679                 if (config.delay > 0) {
680                     Thread.sleep(config.delay); // introduce delay to throttle send rate
681                 }
682             }
683         } catch (InterruptedException e) {
684             Thread.currentThread().interrupt();
685         } finally {
686             logger.debug("{} - Command sender thread exiting", handler.getThingID());
687         }
688     }
689
690     private void shimSenderThreadJob() {
691         logger.debug("Shim sender thread started");
692         try {
693             while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
694                 ShieldTVCommand command = shimQueue.take();
695
696                 try {
697                     BufferedWriter writer = this.shimWriter;
698                     if (writer != null) {
699                         logger.trace("Shim received from shield: {}",
700                                 ShieldTVRequest.decodeMessage(command.toString()));
701                         writer.write(command.toString());
702                         writer.flush();
703                     }
704                 } catch (InterruptedIOException e) {
705                     logger.debug("Shim interrupted while sending.");
706                     break; // exit loop and terminate thread
707                 } catch (IOException e) {
708                     logger.warn("Shim communication error. Error: {}", e.getMessage());
709                     break; // reconnect() will start a new thread; terminate this one
710                 }
711             }
712         } catch (InterruptedException e) {
713             Thread.currentThread().interrupt();
714         } finally {
715             logger.debug("Command sender thread exiting");
716         }
717     }
718
719     private void flushReader() {
720         if (!inMessage && (sbReader.length() > 0)) {
721             sbReader.setLength(sbReader.length() - 2);
722             messageParser.handleMessage(sbReader.toString());
723             if (config.shim) {
724                 sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
725             }
726             sbReader.setLength(0);
727             sbReader.append(lastMsg);
728         }
729         sbReader.append(thisMsg);
730         lastMsg = thisMsg;
731     }
732
733     private void finishReaderMessage() {
734         sbReader.append(thisMsg);
735         lastMsg = "";
736         inMessage = false;
737         messageParser.handleMessage(sbReader.toString());
738         if (config.shim) {
739             sendShim(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbReader.toString())));
740         }
741         sbReader.setLength(0);
742     }
743
744     private String fixMessage(String tempMsg) {
745         if (tempMsg.length() % 2 > 0) {
746             tempMsg = "0" + tempMsg;
747         }
748         return tempMsg;
749     }
750
751     /**
752      * Method executed by the message reader thread (readerThread)
753      */
754     private void readerThreadJob() {
755         logger.debug("{} - Message reader thread started", handler.getThingID());
756         try {
757             BufferedReader reader = this.reader;
758             while (!Thread.interrupted() && reader != null) {
759                 thisMsg = fixMessage(Integer.toHexString(reader.read()));
760                 if (HARD_DROP.equals(thisMsg)) {
761                     // Shield has crashed the connection. Disconnect hard.
762                     logger.debug("{} - readerThreadJob received ffffffff.  Disconnecting hard.", handler.getThingID());
763                     this.isLoggedIn = false;
764                     reconnect();
765                     break;
766                 }
767                 if (DELIMITER_08.equals(lastMsg) && !inMessage) {
768                     flushReader();
769                     inMessage = true;
770                     msgType = thisMsg;
771                 } else if (DELIMITER_18.equals(lastMsg) && thisMsg.equals(msgType) && inMessage) {
772                     if (!msgType.startsWith(DELIMITER_0)) {
773                         sbReader.append(thisMsg);
774                         thisMsg = fixMessage(Integer.toHexString(reader.read()));
775                     }
776                     finishReaderMessage();
777                 } else if (DELIMITER_00.equals(msgType) && (sbReader.toString().length() == 16)) {
778                     // keepalive messages don't have delimiters but are always 18 in length
779                     finishReaderMessage();
780                 } else {
781                     sbReader.append(thisMsg);
782                     lastMsg = thisMsg;
783                 }
784             }
785         } catch (InterruptedIOException e) {
786             logger.debug("Interrupted while reading");
787             setStatus(false, "offline.interrupted");
788         } catch (IOException e) {
789             logger.debug("I/O error while reading from stream: {}", e.getMessage());
790             setStatus(false, "offline.io-error");
791         } catch (RuntimeException e) {
792             logger.warn("Runtime exception in reader thread", e);
793             setStatus(false, "offline.runtime-exception");
794         } finally {
795             logger.debug("{} - Message reader thread exiting", handler.getThingID());
796         }
797     }
798
799     private void shimReaderThreadJob() {
800         logger.debug("Shim reader thread started");
801         String thisShimMsg = "";
802         int thisShimRawMsg = 0;
803         int payloadRemain = 0;
804         int payloadBlock = 0;
805         String thisShimMsgType = "";
806         boolean inShimMessage = false;
807         try {
808             BufferedReader reader = this.shimReader;
809             while (!Thread.interrupted() && reader != null) {
810                 thisShimRawMsg = reader.read();
811                 thisShimMsg = fixMessage(Integer.toHexString(thisShimRawMsg));
812                 if (HARD_DROP.equals(thisShimMsg)) {
813                     disconnect(false);
814                     break;
815                 }
816                 if (!inShimMessage) {
817                     // Beginning of payload
818                     sbShimReader.setLength(0);
819                     sbShimReader.append(thisShimMsg);
820                     inShimMessage = true;
821                     payloadBlock++;
822                 } else if ((payloadBlock == 1) && (DELIMITER_00.equals(thisShimMsg))) {
823                     sbShimReader.append(thisShimMsg);
824                     payloadRemain = 8;
825                     thisShimMsgType = thisShimMsg;
826                     while (payloadRemain > 1) {
827                         thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
828                         sbShimReader.append(thisShimMsg);
829                         payloadRemain--;
830                         payloadBlock++;
831                     }
832                     payloadRemain--;
833                     payloadBlock++;
834                 } else if ((payloadBlock == 1)
835                         && (thisShimMsg.startsWith(DELIMITER_F1) || thisShimMsg.startsWith(DELIMITER_F3))) {
836                     sbShimReader.append(thisShimMsg);
837                     payloadRemain = 6;
838                     thisShimMsgType = thisShimMsg;
839                     while (payloadRemain > 1) {
840                         thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
841                         sbShimReader.append(thisShimMsg);
842                         payloadRemain--;
843                         payloadBlock++;
844                     }
845                     payloadRemain--;
846                     payloadBlock++;
847                 } else if (payloadBlock == 1) {
848                     thisShimMsgType = thisShimMsg;
849                     sbShimReader.append(thisShimMsg);
850                     payloadBlock++;
851                 } else if (payloadBlock == 2) {
852                     sbShimReader.append(thisShimMsg);
853                     payloadBlock++;
854                 } else if (payloadBlock == 3) {
855                     // Length of remainder of packet
856                     payloadRemain = thisShimRawMsg;
857                     sbShimReader.append(thisShimMsg);
858                     payloadBlock++;
859                 } else if (payloadBlock == 4) {
860                     sbShimReader.append(thisShimMsg);
861                     logger.trace("PB4 SSR {} TSMT {} TSM {} PR {}", sbShimReader.toString(), thisShimMsgType,
862                             thisShimMsg, payloadRemain);
863                     if (DELIMITER_E9.equals(thisShimMsgType) || DELIMITER_F0.equals(thisShimMsgType)
864                             || DELIMITER_EC.equals(thisShimMsgType)) {
865                         payloadRemain = thisShimRawMsg + 1;
866                     }
867                     while (payloadRemain > 1) {
868                         thisShimMsg = fixMessage(Integer.toHexString(reader.read()));
869                         sbShimReader.append(thisShimMsg);
870                         payloadRemain--;
871                         payloadBlock++;
872                     }
873                     payloadRemain--;
874                     payloadBlock++;
875                 }
876
877                 if ((payloadBlock > 5) && (payloadRemain == 0)) {
878                     logger.trace("Shim sending to shield: {}", sbShimReader.toString());
879                     sendQueue.add(new ShieldTVCommand(ShieldTVRequest.encodeMessage(sbShimReader.toString())));
880                     inShimMessage = false;
881                     payloadBlock = 0;
882                     payloadRemain = 0;
883                     sbShimReader.setLength(0);
884                 }
885             }
886         } catch (InterruptedIOException e) {
887             logger.debug("Interrupted while reading");
888             setStatus(false, "offline.interrupted");
889         } catch (IOException e) {
890             logger.debug("I/O error while reading from stream: {}", e.getMessage());
891             setStatus(false, "offline.io-error");
892         } catch (RuntimeException e) {
893             logger.warn("Runtime exception in reader thread", e);
894             setStatus(false, "offline.runtime-exception");
895         } finally {
896             logger.debug("Message reader thread exiting");
897         }
898     }
899
900     private void sendKeepAlive() {
901         logger.trace("{} - Sending ShieldTV keepalive query", handler.getThingID());
902         String keepalive = ShieldTVRequest.encodeMessage(ShieldTVRequest.keepAlive());
903         sendCommand(new ShieldTVCommand(keepalive));
904         if (isLoggedIn) {
905             sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08ec0712020806"))); // Get App
906             if (this.periodicUpdate <= 1) {
907                 sendPeriodicUpdate();
908                 this.periodicUpdate = 20;
909             } else {
910                 periodicUpdate--;
911             }
912         }
913         reconnectTaskSchedule();
914     }
915
916     /**
917      * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
918      * be
919      * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
920      */
921     private void reconnectTaskSchedule() {
922         synchronized (keepAliveReconnectLock) {
923             keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
924                     TimeUnit.SECONDS);
925         }
926     }
927
928     /**
929      * Cancels the reconnect task keepAliveReconnectJob.
930      */
931     private void reconnectTaskCancel(boolean interrupt) {
932         synchronized (keepAliveReconnectLock) {
933             ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
934             if (keepAliveReconnectJob != null) {
935                 logger.trace("{} - Canceling ShieldTV scheduled reconnect job.", handler.getThingID());
936                 keepAliveReconnectJob.cancel(interrupt);
937                 this.keepAliveReconnectJob = null;
938             }
939         }
940     }
941
942     /**
943      * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
944      * validMessageReceived() which in turn calls reconnectTaskCancel().
945      */
946     private void keepAliveTimeoutExpired() {
947         logger.debug("{} - ShieldTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
948         reconnect();
949     }
950
951     public void validMessageReceived() {
952         reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
953     }
954
955     public void sendCommand(ShieldTVCommand command) {
956         if ((!config.shim) && (!command.isEmpty())) {
957             sendQueue.add(command);
958         }
959     }
960
961     public void sendShim(ShieldTVCommand command) {
962         if (!command.isEmpty()) {
963             shimQueue.add(command);
964         }
965     }
966
967     public void handleCommand(ChannelUID channelUID, Command command) {
968         logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
969
970         if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
971             if (command instanceof StringType) {
972                 switch (command.toString()) {
973                     case "KEY_UP":
974                         sendCommand(new ShieldTVCommand(
975                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
976                         sendCommand(new ShieldTVCommand(
977                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
978                         break;
979                     case "KEY_DOWN":
980                         sendCommand(new ShieldTVCommand(
981                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
982                         sendCommand(new ShieldTVCommand(
983                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
984                         break;
985                     case "KEY_RIGHT":
986                         sendCommand(new ShieldTVCommand(
987                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
988                         sendCommand(new ShieldTVCommand(
989                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
990                         break;
991                     case "KEY_LEFT":
992                         sendCommand(new ShieldTVCommand(
993                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
994                         sendCommand(new ShieldTVCommand(
995                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
996                         break;
997                     case "KEY_ENTER":
998                         sendCommand(new ShieldTVCommand(
999                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1000                         sendCommand(new ShieldTVCommand(
1001                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1002                         break;
1003                     case "KEY_HOME":
1004                         sendCommand(new ShieldTVCommand(
1005                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1006                         sendCommand(new ShieldTVCommand(
1007                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1008                         break;
1009                     case "KEY_BACK":
1010                         sendCommand(new ShieldTVCommand(
1011                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1012                         sendCommand(new ShieldTVCommand(
1013                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1014                         break;
1015                     case "KEY_MENU":
1016                         sendCommand(new ShieldTVCommand(
1017                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1018                         sendCommand(new ShieldTVCommand(
1019                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1020                         break;
1021                     case "KEY_PLAYPAUSE":
1022                         sendCommand(new ShieldTVCommand(
1023                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1024                         sendCommand(new ShieldTVCommand(
1025                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1026                         break;
1027                     case "KEY_REWIND":
1028                         sendCommand(new ShieldTVCommand(
1029                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1030                         sendCommand(new ShieldTVCommand(
1031                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1032                         break;
1033                     case "KEY_FORWARD":
1034                         sendCommand(new ShieldTVCommand(
1035                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1036                         sendCommand(new ShieldTVCommand(
1037                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1038                         break;
1039                     case "KEY_UP_PRESS":
1040                         sendCommand(new ShieldTVCommand(
1041                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202ce01")));
1042                         break;
1043                     case "KEY_DOWN_PRESS":
1044                         sendCommand(new ShieldTVCommand(
1045                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d801")));
1046                         break;
1047                     case "KEY_RIGHT_PRESS":
1048                         sendCommand(new ShieldTVCommand(
1049                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d401")));
1050                         break;
1051                     case "KEY_LEFT_PRESS":
1052                         sendCommand(new ShieldTVCommand(
1053                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d201")));
1054                         break;
1055                     case "KEY_ENTER_PRESS":
1056                         sendCommand(new ShieldTVCommand(
1057                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202c205")));
1058                         break;
1059                     case "KEY_HOME_PRESS":
1060                         sendCommand(new ShieldTVCommand(
1061                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202d802")));
1062                         break;
1063                     case "KEY_BACK_PRESS":
1064                         sendCommand(new ShieldTVCommand(
1065                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202bc02")));
1066                         break;
1067                     case "KEY_MENU_PRESS":
1068                         sendCommand(new ShieldTVCommand(
1069                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280132029602")));
1070                         break;
1071                     case "KEY_PLAYPAUSE_PRESS":
1072                         sendCommand(new ShieldTVCommand(
1073                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202F604")));
1074                         break;
1075                     case "KEY_REWIND_PRESS":
1076                         sendCommand(new ShieldTVCommand(
1077                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202D002")));
1078                         break;
1079                     case "KEY_FORWARD_PRESS":
1080                         sendCommand(new ShieldTVCommand(
1081                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28013202A003")));
1082                         break;
1083                     case "KEY_UP_RELEASE":
1084                         sendCommand(new ShieldTVCommand(
1085                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202ce01")));
1086                         break;
1087                     case "KEY_DOWN_RELEASE":
1088                         sendCommand(new ShieldTVCommand(
1089                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d801")));
1090                         break;
1091                     case "KEY_RIGHT_RELEASE":
1092                         sendCommand(new ShieldTVCommand(
1093                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d401")));
1094                         break;
1095                     case "KEY_LEFT_RELEASE":
1096                         sendCommand(new ShieldTVCommand(
1097                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d201")));
1098                         break;
1099                     case "KEY_ENTER_RELEASE":
1100                         sendCommand(new ShieldTVCommand(
1101                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202c205")));
1102                         break;
1103                     case "KEY_HOME_RELEASE":
1104                         sendCommand(new ShieldTVCommand(
1105                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202d802")));
1106                         break;
1107                     case "KEY_BACK_RELEASE":
1108                         sendCommand(new ShieldTVCommand(
1109                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202bc02")));
1110                         break;
1111                     case "KEY_MENU_RELEASE":
1112                         sendCommand(new ShieldTVCommand(
1113                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a280232029602")));
1114                         break;
1115                     case "KEY_PLAYPAUSE_RELEASE":
1116                         sendCommand(new ShieldTVCommand(
1117                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202F604")));
1118                         break;
1119                     case "KEY_REWIND_RELEASE":
1120                         sendCommand(new ShieldTVCommand(
1121                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202D002")));
1122                         break;
1123                     case "KEY_FORWARD_RELEASE":
1124                         sendCommand(new ShieldTVCommand(
1125                                 ShieldTVRequest.encodeMessage("08e907120c08141001200a28023202A003")));
1126                         break;
1127                     case "KEY_POWER":
1128                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401e")));
1129                         break;
1130                     case "KEY_POWERON":
1131                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e4010")));
1132                         break;
1133                     case "KEY_GOOGLE":
1134                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e907120808141005201e401f")));
1135                         break;
1136                     case "KEY_VOLUP":
1137                         sendCommand(new ShieldTVCommand(
1138                                 ShieldTVRequest.encodeMessage("08f007120c08031208080110031a020102")));
1139                         break;
1140                     case "KEY_VOLDOWN":
1141                         sendCommand(new ShieldTVCommand(
1142                                 ShieldTVRequest.encodeMessage("08f007120c08031208080110011a020102")));
1143                         break;
1144                     case "KEY_MUTE":
1145                         sendCommand(new ShieldTVCommand(
1146                                 ShieldTVRequest.encodeMessage("08f007120c08031208080110021a020102")));
1147                         break;
1148                     case "KEY_SUBMIT":
1149                         sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1150                         break;
1151                 }
1152                 if (command.toString().length() == 5) {
1153                     // Account for KEY_(ASCII Character)
1154                     String keyPress = "08ec07120708011201"
1155                             + ShieldTVRequest.decodeMessage(new String("" + command.toString().charAt(4))) + "1801";
1156                     sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage(keyPress)));
1157                 } else {
1158                     logger.trace("Unknown Keypress: {}", command.toString());
1159                 }
1160             }
1161         } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1162             if (command instanceof StringType) {
1163                 if (!isLoggedIn) {
1164                     // Do PIN for shieldtv protocol
1165                     logger.debug("{} - ShieldTV PIN Process Started", handler.getThingID());
1166                     String pin = ShieldTVRequest.pinRequest(command.toString());
1167                     String message = ShieldTVRequest.encodeMessage(pin);
1168                     sendCommand(new ShieldTVCommand(message));
1169                 }
1170             }
1171         } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1172             if (command instanceof StringType) {
1173                 if (command.toString().startsWith("RAW", 9)) {
1174                     String newCommand = command.toString().substring(13);
1175                     String message = ShieldTVRequest.encodeMessage(newCommand);
1176                     if (logger.isTraceEnabled()) {
1177                         logger.trace("Raw Message Decodes as: {}", ShieldTVRequest.decodeMessage(message));
1178                     }
1179                     sendCommand(new ShieldTVCommand(message));
1180                 } else if (command.toString().startsWith("MSG", 9)) {
1181                     String newCommand = command.toString().substring(13);
1182                     messageParser.handleMessage(newCommand);
1183                 }
1184             }
1185         } else if (CHANNEL_APP.equals(channelUID.getId())) {
1186             if (command instanceof StringType) {
1187                 String message = ShieldTVRequest.encodeMessage(ShieldTVRequest.startApp(command.toString()));
1188                 sendCommand(new ShieldTVCommand(message));
1189             }
1190         } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1191             if (command instanceof StringType) {
1192                 String entry = ShieldTVRequest.keyboardEntry(command.toString());
1193                 logger.trace("Keyboard Entry {}", entry);
1194                 String message = ShieldTVRequest.encodeMessage(entry);
1195                 sendCommand(new ShieldTVCommand(message));
1196                 sendCommand(new ShieldTVCommand(ShieldTVRequest.encodeMessage("08e9071209081410012001320138")));
1197             }
1198         }
1199     }
1200
1201     public void dispose() {
1202         this.disposing = true;
1203
1204         Future<?> asyncInitializeTask = this.asyncInitializeTask;
1205         if (asyncInitializeTask != null) {
1206             asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1207         }
1208         Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1209         if (shimAsyncInitializeTask != null) {
1210             shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1211         }
1212         ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1213         if (deviceHealthJob != null) {
1214             deviceHealthJob.cancel(true);
1215         }
1216         disconnect(true);
1217     }
1218 }