]> git.basschouten.com Git - openhab-addons.git/blob
3a2dd4e87aabb4e502e801eb512144ee1c4394ba
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.googletv;
14
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConstants.*;
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.Socket;
30 import java.net.SocketAddress;
31 import java.net.SocketTimeoutException;
32 import java.net.UnknownHostException;
33 import java.nio.charset.StandardCharsets;
34 import java.security.GeneralSecurityException;
35 import java.security.NoSuchAlgorithmException;
36 import java.security.UnrecoverableKeyException;
37 import java.security.cert.Certificate;
38 import java.security.cert.CertificateException;
39 import java.security.cert.X509Certificate;
40 import java.util.concurrent.BlockingQueue;
41 import java.util.concurrent.Future;
42 import java.util.concurrent.LinkedBlockingQueue;
43 import java.util.concurrent.ScheduledExecutorService;
44 import java.util.concurrent.ScheduledFuture;
45 import java.util.concurrent.TimeUnit;
46
47 import javax.net.ssl.KeyManagerFactory;
48 import javax.net.ssl.SSLContext;
49 import javax.net.ssl.SSLServerSocket;
50 import javax.net.ssl.SSLServerSocketFactory;
51 import javax.net.ssl.SSLSession;
52 import javax.net.ssl.SSLSocket;
53 import javax.net.ssl.SSLSocketFactory;
54 import javax.net.ssl.TrustManager;
55 import javax.net.ssl.X509TrustManager;
56
57 import org.eclipse.jdt.annotation.NonNullByDefault;
58 import org.eclipse.jdt.annotation.Nullable;
59 import org.openhab.binding.androidtv.internal.AndroidTVHandler;
60 import org.openhab.binding.androidtv.internal.AndroidTVTranslationProvider;
61 import org.openhab.binding.androidtv.internal.utils.AndroidTVPKI;
62 import org.openhab.core.OpenHAB;
63 import org.openhab.core.library.types.NextPreviousType;
64 import org.openhab.core.library.types.OnOffType;
65 import org.openhab.core.library.types.PercentType;
66 import org.openhab.core.library.types.PlayPauseType;
67 import org.openhab.core.library.types.RewindFastforwardType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.thing.ChannelUID;
70 import org.openhab.core.types.Command;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
73
74 /**
75  * The {@link GoogleTVConnectionManager} is responsible for handling connections via the googletv protocol
76  *
77  * Significant portions reused from Lutron binding with permission from Bob A.
78  *
79  * @author Ben Rosenblum - Initial contribution
80  */
81 @NonNullByDefault
82 public class GoogleTVConnectionManager {
83     private static final int DEFAULT_RECONNECT_SECONDS = 60;
84     private static final int DEFAULT_HEARTBEAT_SECONDS = 5;
85     private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
86     private static final String DEFAULT_KEYSTORE_PASSWORD = "secret";
87     private static final String DEFAULT_MODE = "NORMAL";
88     private static final String PIN_MODE = "PIN";
89     private static final int DEFAULT_PORT = 6466;
90     private static final int PIN_DELAY = 1000;
91
92     private final Logger logger = LoggerFactory.getLogger(GoogleTVConnectionManager.class);
93
94     private ScheduledExecutorService scheduler;
95
96     private final AndroidTVHandler handler;
97     private GoogleTVConfiguration config;
98     private final AndroidTVTranslationProvider translationProvider;
99
100     private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
101     private @Nullable SSLSocket sslSocket;
102     private @Nullable BufferedWriter writer;
103     private @Nullable BufferedReader reader;
104
105     private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
106     private @Nullable Socket shimServerSocket;
107     private @Nullable BufferedWriter shimWriter;
108     private @Nullable BufferedReader shimReader;
109
110     private @Nullable GoogleTVConnectionManager connectionManager;
111     private @Nullable GoogleTVConnectionManager childConnectionManager;
112     private @NonNullByDefault({}) GoogleTVMessageParser messageParser;
113
114     private final BlockingQueue<GoogleTVCommand> sendQueue = new LinkedBlockingQueue<>();
115     private final BlockingQueue<GoogleTVCommand> shimQueue = new LinkedBlockingQueue<>();
116
117     private @Nullable Future<?> asyncInitializeTask;
118     private @Nullable Future<?> shimAsyncInitializeTask;
119
120     private @Nullable Thread senderThread;
121     private @Nullable Thread readerThread;
122     private @Nullable Thread shimSenderThread;
123     private @Nullable Thread shimReaderThread;
124
125     private @Nullable ScheduledFuture<?> keepAliveJob;
126     private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
127     private @Nullable ScheduledFuture<?> connectRetryJob;
128     private final Object keepAliveReconnectLock = new Object();
129     private final Object connectionLock = new Object();
130
131     private @Nullable ScheduledFuture<?> deviceHealthJob;
132     private boolean isOnline = true;
133
134     private StringBuffer sbReader = new StringBuffer();
135     private StringBuffer sbShimReader = new StringBuffer();
136     private String thisMsg = "";
137
138     private X509Certificate @Nullable [] shimX509ClientChain;
139     private Certificate @Nullable [] shimClientChain;
140     private Certificate @Nullable [] shimServerChain;
141     private Certificate @Nullable [] shimClientLocalChain;
142
143     private boolean disposing = false;
144     private boolean isLoggedIn = false;
145     private String statusMessage = "";
146     private String pinHash = "";
147     private String shimPinHash = "";
148
149     private boolean power = false;
150     private String volCurr = "00";
151     private String volMax = "ff";
152     private boolean volMute = false;
153     private String audioMode = "";
154     private String currentApp = "";
155     private String manufacturer = "";
156     private String model = "";
157     private String androidVersion = "";
158     private String remoteServer = "";
159     private String remoteServerVersion = "";
160
161     private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
162     private byte[] encryptionKey;
163
164     public GoogleTVConnectionManager(AndroidTVHandler handler, GoogleTVConfiguration config) {
165         messageParser = new GoogleTVMessageParser(this);
166         this.config = config;
167         this.handler = handler;
168         this.translationProvider = handler.getTranslationProvider();
169         this.connectionManager = this;
170         this.scheduler = handler.getScheduler();
171         this.encryptionKey = androidtvPKI.generateEncryptionKey();
172         initialize();
173     }
174
175     public GoogleTVConnectionManager(AndroidTVHandler handler, GoogleTVConfiguration config,
176             GoogleTVConnectionManager connectionManager) {
177         messageParser = new GoogleTVMessageParser(this);
178         this.config = config;
179         this.handler = handler;
180         this.translationProvider = handler.getTranslationProvider();
181         this.connectionManager = connectionManager;
182         this.scheduler = handler.getScheduler();
183         this.encryptionKey = androidtvPKI.generateEncryptionKey();
184         initialize();
185     }
186
187     public AndroidTVHandler getHandler() {
188         return handler;
189     }
190
191     public String getThingID() {
192         return handler.getThingID();
193     }
194
195     public void setManufacturer(String manufacturer) {
196         this.manufacturer = manufacturer;
197         handler.setThingProperty("manufacturer", manufacturer);
198     }
199
200     public String getManufacturer() {
201         return manufacturer;
202     }
203
204     public void setModel(String model) {
205         this.model = model;
206         handler.setThingProperty("model", model);
207     }
208
209     public String getModel() {
210         return model;
211     }
212
213     public void setAndroidVersion(String androidVersion) {
214         this.androidVersion = androidVersion;
215         handler.setThingProperty("androidVersion", androidVersion);
216     }
217
218     public String getAndroidVersion() {
219         return androidVersion;
220     }
221
222     public void setRemoteServer(String remoteServer) {
223         this.remoteServer = remoteServer;
224         handler.setThingProperty("remoteServer", remoteServer);
225     }
226
227     public String getRemoteServer() {
228         return remoteServer;
229     }
230
231     public void setRemoteServerVersion(String remoteServerVersion) {
232         this.remoteServerVersion = remoteServerVersion;
233         handler.setThingProperty("remoteServerVersion", remoteServerVersion);
234     }
235
236     public String getRemoteServerVersion() {
237         return remoteServerVersion;
238     }
239
240     public void setPower(boolean power) {
241         this.power = power;
242         logger.debug("{} - Setting power to {}", handler.getThingID(), power);
243         if (power) {
244             handler.updateChannelState(CHANNEL_POWER, OnOffType.ON);
245         } else {
246             handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
247         }
248     }
249
250     public boolean getPower() {
251         return power;
252     }
253
254     public void setVolCurr(String volCurr) {
255         this.volCurr = volCurr;
256         int max = Integer.parseInt(this.volMax, 16);
257         int volume = ((Integer.parseInt(volCurr, 16) * 100) / max);
258         handler.updateChannelState(CHANNEL_VOLUME, new PercentType(volume));
259     }
260
261     public String getVolCurr() {
262         return volCurr;
263     }
264
265     public void setVolMax(String volMax) {
266         this.volMax = volMax;
267     }
268
269     public String getVolMax() {
270         return volMax;
271     }
272
273     public void setVolMute(String volMute) {
274         if (DELIMITER_00.equals(volMute)) {
275             this.volMute = false;
276             handler.updateChannelState(CHANNEL_MUTE, OnOffType.OFF);
277         } else if (DELIMITER_01.equals(volMute)) {
278             this.volMute = true;
279             handler.updateChannelState(CHANNEL_MUTE, OnOffType.ON);
280         }
281     }
282
283     public boolean getVolMute() {
284         return volMute;
285     }
286
287     public void setAudioMode(String audioMode) {
288         this.audioMode = audioMode;
289     }
290
291     public String getAudioMode() {
292         return audioMode;
293     }
294
295     public void setCurrentApp(String currentApp) {
296         this.currentApp = currentApp;
297         handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
298     }
299
300     public String getStatusMessage() {
301         return statusMessage;
302     }
303
304     private void setStatus(boolean isLoggedIn) {
305         if (isLoggedIn) {
306             setStatus(isLoggedIn, "online.online");
307         } else {
308             setStatus(isLoggedIn, "offline.unknown");
309         }
310     }
311
312     private void setStatus(boolean isLoggedIn, String statusMessage) {
313         String translatedMessage = translationProvider.getText(statusMessage);
314         if ((this.isLoggedIn != isLoggedIn) || (!this.statusMessage.equals(translatedMessage))) {
315             this.isLoggedIn = isLoggedIn;
316             this.statusMessage = translatedMessage;
317             handler.checkThingStatus();
318         }
319     }
320
321     public String getCurrentApp() {
322         return currentApp;
323     }
324
325     public void setLoggedIn(boolean isLoggedIn) {
326         if (this.isLoggedIn != isLoggedIn) {
327             setStatus(isLoggedIn);
328         }
329     }
330
331     public boolean getLoggedIn() {
332         return isLoggedIn;
333     }
334
335     private boolean servicePing() {
336         int timeout = 500;
337
338         SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
339         try (Socket socket = new Socket()) {
340             socket.connect(socketAddress, timeout);
341             return true;
342         } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
343             return false;
344         } catch (IOException ignored) {
345             // IOException is thrown by automatic close() of the socket.
346             // This should actually never return a value as we should return true above already
347             return true;
348         }
349     }
350
351     private void checkHealth() {
352         boolean isOnline;
353         if (!isLoggedIn) {
354             isOnline = servicePing();
355         } else {
356             isOnline = true;
357         }
358         logger.debug("{} - Device Health - Online: {} - Logged In: {} - Mode: {}", handler.getThingID(), isOnline,
359                 isLoggedIn, config.mode);
360         if (isOnline != this.isOnline) {
361             this.isOnline = isOnline;
362             if (isOnline) {
363                 logger.debug("{} - Device is back online.  Attempting reconnection.", handler.getThingID());
364                 reconnect();
365             }
366         }
367     }
368
369     private void setShimX509ClientChain(X509Certificate @Nullable [] shimX509ClientChain) {
370         try {
371             this.shimX509ClientChain = shimX509ClientChain;
372             logger.trace("Setting shimX509ClientChain {}", config.port);
373             if (shimX509ClientChain != null) {
374                 if (logger.isTraceEnabled()) {
375                     logger.trace("Subject DN: {}", shimX509ClientChain[0].getSubjectX500Principal());
376                     logger.trace("Issuer DN: {}", shimX509ClientChain[0].getIssuerX500Principal());
377                     logger.trace("Serial number: {}", shimX509ClientChain[0].getSerialNumber());
378                     logger.trace("Cert: {}", GoogleTVRequest
379                             .decodeMessage(GoogleTVUtils.byteArrayToString(shimX509ClientChain[0].getEncoded())));
380                 }
381                 androidtvPKI.setCaCert(shimX509ClientChain[0]);
382                 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
383             }
384         } catch (Exception e) {
385             logger.trace("setShimX509ClientChain Exception", e);
386         }
387     }
388
389     private void startChildConnectionManager(int port, String mode) {
390         GoogleTVConfiguration childConfig = new GoogleTVConfiguration();
391         childConfig.ipAddress = config.ipAddress;
392         childConfig.port = port;
393         childConfig.reconnect = config.reconnect;
394         childConfig.heartbeat = config.heartbeat;
395         childConfig.keystoreFileName = config.keystoreFileName;
396         childConfig.keystorePassword = config.keystorePassword;
397         childConfig.delay = config.delay;
398         childConfig.shim = config.shim;
399         childConfig.mode = mode;
400         logger.debug("{} - startChildConnectionManager parent config: {} {} {}", handler.getThingID(), config.port,
401                 config.mode, config.shim);
402         logger.debug("{} - startChildConnectionManager child config: {} {} {}", handler.getThingID(), childConfig.port,
403                 childConfig.mode, childConfig.shim);
404         childConnectionManager = new GoogleTVConnectionManager(this.handler, childConfig, this);
405     }
406
407     private TrustManager[] defineNoOpTrustManager() {
408         return new TrustManager[] { new X509TrustManager() {
409             @Override
410             public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
411                 logger.debug("Assuming client certificate is valid");
412                 if (chain != null && logger.isTraceEnabled()) {
413                     for (int cert = 0; cert < chain.length; cert++) {
414                         logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
415                         logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
416                         logger.trace("Serial number: {}", chain[cert].getSerialNumber());
417                     }
418                 }
419             }
420
421             @Override
422             public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
423                 logger.debug("Assuming server certificate is valid");
424                 if (chain != null && logger.isTraceEnabled()) {
425                     for (int cert = 0; cert < chain.length; cert++) {
426                         logger.trace("Subject DN: {}", chain[cert].getSubjectX500Principal());
427                         logger.trace("Issuer DN: {}", chain[cert].getIssuerX500Principal());
428                         logger.trace("Serial number: {}", chain[cert].getSerialNumber());
429                     }
430                 }
431             }
432
433             @Override
434             public X509Certificate @Nullable [] getAcceptedIssuers() {
435                 X509Certificate[] x509ClientChain = shimX509ClientChain;
436                 if (x509ClientChain != null && logger.isTraceEnabled()) {
437                     logger.debug("Returning shimX509ClientChain for getAcceptedIssuers");
438                     for (int cert = 0; cert < x509ClientChain.length; cert++) {
439                         logger.trace("Subject DN: {}", x509ClientChain[cert].getSubjectX500Principal());
440                         logger.trace("Issuer DN: {}", x509ClientChain[cert].getIssuerX500Principal());
441                         logger.trace("Serial number: {}", x509ClientChain[cert].getSerialNumber());
442                     }
443                     return x509ClientChain;
444                 } else {
445                     logger.debug("Returning empty certificate for getAcceptedIssuers");
446                     return new X509Certificate[0];
447                 }
448             }
449         } };
450     }
451
452     private void initialize() {
453         SSLContext sslContext;
454
455         String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
456         File folder = new File(folderName);
457
458         if (!folder.exists()) {
459             logger.debug("Creating directory {}", folderName);
460             folder.mkdirs();
461         }
462
463         config.port = (config.port > 0) ? config.port : DEFAULT_PORT;
464         config.mode = (!config.mode.equals("")) ? config.mode : DEFAULT_MODE;
465
466         config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
467                 : folderName + "/googletv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
468                         + ".keystore";
469         config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
470                 : DEFAULT_KEYSTORE_PASSWORD;
471
472         androidtvPKI.setKeystoreFileName(config.keystoreFileName);
473         androidtvPKI.setAlias("nvidia");
474
475         if (config.mode.equals(DEFAULT_MODE)) {
476             deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
477                     TimeUnit.SECONDS);
478         }
479
480         try {
481             File keystoreFile = new File(config.keystoreFileName);
482
483             if (!keystoreFile.exists() || config.shimNewKeys) {
484                 androidtvPKI.generateNewKeyPair(encryptionKey);
485                 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
486             } else {
487                 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
488             }
489
490             logger.trace("{} - Initializing SSL Context", handler.getThingID());
491             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
492             kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
493                     config.keystorePassword.toCharArray());
494
495             TrustManager[] trustManagers = defineNoOpTrustManager();
496
497             sslContext = SSLContext.getInstance("TLS");
498             sslContext.init(kmf.getKeyManagers(), trustManagers, null);
499
500             sslSocketFactory = sslContext.getSocketFactory();
501             if (!config.shim) {
502                 asyncInitializeTask = scheduler.submit(this::connect);
503             } else {
504                 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
505             }
506         } catch (NoSuchAlgorithmException | IOException e) {
507             setStatus(false, "offline.error-initalizing-keystore");
508             logger.debug("Error initializing keystore", e);
509         } catch (UnrecoverableKeyException e) {
510             setStatus(false, "offline.key-unrecoverable-with-supplied-password");
511         } catch (GeneralSecurityException e) {
512             logger.debug("General security exception", e);
513         } catch (Exception e) {
514             logger.debug("General exception", e);
515         }
516     }
517
518     public void connect() {
519         synchronized (connectionLock) {
520             if (isOnline || config.mode.equals(PIN_MODE)) {
521                 try {
522                     logger.debug("{} - Opening GoogleTV SSL connection to {}:{} {}", handler.getThingID(),
523                             config.ipAddress, config.port, config.mode);
524                     SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
525                     sslSocket.startHandshake();
526                     this.shimServerChain = ((SSLSocket) sslSocket).getSession().getPeerCertificates();
527                     writer = new BufferedWriter(
528                             new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
529                     reader = new BufferedReader(
530                             new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
531                     this.sslSocket = sslSocket;
532                     this.sendQueue.clear();
533                     logger.debug("{} - Connection to {}:{} {} successful", handler.getThingID(), config.ipAddress,
534                             config.port, config.mode);
535                 } catch (UnknownHostException e) {
536                     setStatus(false, "offline.unknown-host");
537                     logger.debug("{} - Unknown host {}", handler.getThingID(), config.ipAddress);
538                     return;
539                 } catch (IllegalArgumentException e) {
540                     // port out of valid range
541                     setStatus(false, "offline.invalid-port-number");
542                     logger.debug("{} - Invalid port number {}:{}", handler.getThingID(), config.ipAddress, config.port);
543                     return;
544                 } catch (InterruptedIOException e) {
545                     logger.debug("{} - Interrupted while establishing GoogleTV connection", handler.getThingID());
546                     Thread.currentThread().interrupt();
547                     return;
548                 } catch (IOException e) {
549                     String message = e.getMessage();
550                     if ((message != null) && (message.contains("certificate_unknown"))
551                             && (!config.mode.equals(PIN_MODE)) && (!config.shim)) {
552                         setStatus(false, "offline.pin-process-incomplete");
553                         logger.debug("{} - GoogleTV PIN Process Incomplete", handler.getThingID());
554                         reconnectTaskCancel(true);
555                         startChildConnectionManager(this.config.port + 1, PIN_MODE);
556                     } else if ((message != null) && (message.contains("certificate_unknown")) && (config.shim)) {
557                         logger.debug("Shim cert_unknown I/O error while connecting: {}", e.getMessage());
558                         Socket shimServerSocket = this.shimServerSocket;
559                         if (shimServerSocket != null) {
560                             try {
561                                 shimServerSocket.close();
562                             } catch (IOException ex) {
563                                 logger.debug("Error closing GoogleTV SSL socket: {}", ex.getMessage());
564                             }
565                             this.shimServerSocket = null;
566                         }
567                     } else {
568                         setStatus(false, "offline.error-opening-ssl-connection-check-log");
569                         logger.info("{} - Error opening SSL connection to {}:{} {}", handler.getThingID(),
570                                 config.ipAddress, config.port, e.getMessage());
571                         disconnect(false);
572                         scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
573                     }
574                     return;
575                 }
576
577                 setStatus(false, "offline.initializing");
578
579                 logger.trace("{} - Starting Reader Thread for {}:{}", handler.getThingID(), config.ipAddress,
580                         config.port);
581
582                 Thread readerThread = new Thread(this::readerThreadJob, "GoogleTV reader " + handler.getThingID());
583                 readerThread.setDaemon(true);
584                 readerThread.start();
585                 this.readerThread = readerThread;
586
587                 logger.trace("{} - Starting Sender Thread for {}:{}", handler.getThingID(), config.ipAddress,
588                         config.port);
589
590                 Thread senderThread = new Thread(this::senderThreadJob, "GoogleTV sender " + handler.getThingID());
591                 senderThread.setDaemon(true);
592                 senderThread.start();
593                 this.senderThread = senderThread;
594
595                 logger.trace("{} - Checking for PIN MODE for {}:{} {}", handler.getThingID(), config.ipAddress,
596                         config.port, config.mode);
597
598                 if (config.mode.equals(PIN_MODE)) {
599                     logger.trace("{} - Sending PIN Login to {}:{}", handler.getThingID(), config.ipAddress,
600                             config.port);
601                     // Send app name and device name
602                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(1))));
603                     // Unknown but required
604                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(2))));
605                     // Don't send pin request yet, let user send REQUEST via PINCODE channel
606                 } else {
607                     logger.trace("{} - Not PIN Mode {}:{} {}", handler.getThingID(), config.ipAddress, config.port,
608                             config.mode);
609                 }
610             } else {
611                 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
612             }
613         }
614     }
615
616     public void shimInitialize() {
617         synchronized (connectionLock) {
618             SSLContext sslContext;
619
620             try {
621                 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
622                 kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
623                         config.keystorePassword.toCharArray());
624
625                 TrustManager[] trustManagers = defineNoOpTrustManager();
626
627                 sslContext = SSLContext.getInstance("TLS");
628                 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
629                 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
630
631                 logger.trace("Opening GoogleTV shim on port {}", config.port);
632                 SSLServerSocket sslServerSocket = (SSLServerSocket) this.sslServerSocketFactory
633                         .createServerSocket(config.port);
634                 if (this.config.mode.equals(DEFAULT_MODE)) {
635                     sslServerSocket.setNeedClientAuth(true);
636                 } else {
637                     sslServerSocket.setWantClientAuth(true);
638                 }
639
640                 while (true) {
641                     logger.trace("Waiting for shim connection... {}", config.port);
642                     if (this.config.mode.equals(DEFAULT_MODE) && (childConnectionManager == null)) {
643                         logger.trace("Starting childConnectionManager {}", config.port);
644                         startChildConnectionManager(this.config.port + 1, PIN_MODE);
645                     }
646                     SSLSocket serverSocket = (SSLSocket) sslServerSocket.accept();
647                     logger.trace("shimInitialize accepted {}", config.port);
648                     try {
649                         serverSocket.startHandshake();
650                         logger.trace("shimInitialize startHandshake {}", config.port);
651                         connect();
652                         logger.trace("shimInitialize connected {}", config.port);
653
654                         SSLSession session = serverSocket.getSession();
655                         Certificate[] cchain2 = session.getPeerCertificates();
656                         this.shimClientChain = cchain2;
657                         Certificate[] cchain3 = session.getLocalCertificates();
658                         this.shimClientLocalChain = cchain3;
659
660                         X509Certificate[] shimX509ClientChain = new X509Certificate[cchain2.length];
661
662                         for (int i = 0; i < cchain2.length; i++) {
663                             logger.trace("Connection from: {}",
664                                     ((X509Certificate) cchain2[i]).getSubjectX500Principal());
665                             shimX509ClientChain[i] = ((X509Certificate) cchain2[i]);
666                             if (this.config.mode.equals(DEFAULT_MODE) && logger.isTraceEnabled()) {
667                                 logger.trace("Cert: {}", GoogleTVRequest.decodeMessage(
668                                         GoogleTVUtils.byteArrayToString(((X509Certificate) cchain2[i]).getEncoded())));
669                             }
670                         }
671
672                         if (this.config.mode.equals(PIN_MODE)) {
673                             this.shimX509ClientChain = shimX509ClientChain;
674                             GoogleTVConnectionManager connectionManager = this.connectionManager;
675                             if (connectionManager != null) {
676                                 connectionManager.setShimX509ClientChain(shimX509ClientChain);
677                             }
678                         }
679
680                         if (cchain3 != null) {
681                             for (int i = 0; i < cchain3.length; i++) {
682                                 logger.trace("Connection from: {}",
683                                         ((X509Certificate) cchain3[i]).getSubjectX500Principal());
684                             }
685                         }
686
687                         logger.trace("Peer host is {}", session.getPeerHost());
688                         logger.trace("Cipher is {}", session.getCipherSuite());
689                         logger.trace("Protocol is {}", session.getProtocol());
690                         logger.trace("ID is {}", new BigInteger(session.getId()));
691                         logger.trace("Session created in {}", session.getCreationTime());
692                         logger.trace("Session accessed in {}", session.getLastAccessedTime());
693
694                         shimWriter = new BufferedWriter(
695                                 new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
696                         shimReader = new BufferedReader(
697                                 new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
698                         this.shimServerSocket = serverSocket;
699                         this.shimQueue.clear();
700
701                         Thread readerThread = new Thread(this::shimReaderThreadJob, "GoogleTV shim reader");
702                         readerThread.setDaemon(true);
703                         readerThread.start();
704                         this.shimReaderThread = readerThread;
705
706                         Thread senderThread = new Thread(this::shimSenderThreadJob, "GoogleTV shim sender");
707                         senderThread.setDaemon(true);
708                         senderThread.start();
709                         this.shimSenderThread = senderThread;
710                     } catch (Exception e) {
711                         logger.trace("Shim initalization exception {}", config.port);
712                         logger.trace("Shim initalization exception", e);
713                     }
714                 }
715             } catch (Exception e) {
716                 logger.trace("Shim initalization exception {}", config.port);
717                 logger.trace("Shim initalization exception", e);
718
719                 return;
720             }
721         }
722     }
723
724     private void scheduleConnectRetry(long waitSeconds) {
725         logger.trace("{} - Scheduling GoogleTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
726         connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
727     }
728
729     /**
730      * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
731      * clean up.
732      *
733      * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
734      *            connect or reconnect, and true when calling from dispose.
735      */
736     private void disconnect(boolean interruptAll) {
737         synchronized (connectionLock) {
738             logger.debug("{} - Disconnecting GoogleTV", handler.getThingID());
739
740             this.isLoggedIn = false;
741
742             ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
743             if (connectRetryJob != null) {
744                 connectRetryJob.cancel(true);
745             }
746             ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
747             if (keepAliveJob != null) {
748                 keepAliveJob.cancel(true);
749             }
750             reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
751
752             Thread senderThread = this.senderThread;
753             if (senderThread != null && senderThread.isAlive()) {
754                 senderThread.interrupt();
755             }
756
757             Thread readerThread = this.readerThread;
758             if (readerThread != null && readerThread.isAlive()) {
759                 readerThread.interrupt();
760             }
761
762             Thread shimSenderThread = this.shimSenderThread;
763             if (shimSenderThread != null && shimSenderThread.isAlive()) {
764                 shimSenderThread.interrupt();
765             }
766
767             Thread shimReaderThread = this.shimReaderThread;
768             if (shimReaderThread != null && shimReaderThread.isAlive()) {
769                 shimReaderThread.interrupt();
770             }
771
772             SSLSocket sslSocket = this.sslSocket;
773             if (sslSocket != null) {
774                 try {
775                     sslSocket.close();
776                 } catch (IOException e) {
777                     logger.debug("Error closing GoogleTV SSL socket: {}", e.getMessage());
778                 }
779                 this.sslSocket = null;
780             }
781             BufferedReader reader = this.reader;
782             if (reader != null) {
783                 try {
784                     reader.close();
785                 } catch (IOException e) {
786                     logger.debug("Error closing reader: {}", e.getMessage());
787                 }
788             }
789             BufferedWriter writer = this.writer;
790             if (writer != null) {
791                 try {
792                     writer.close();
793                 } catch (IOException e) {
794                     logger.debug("Error closing writer: {}", e.getMessage());
795                 }
796             }
797
798             Socket shimServerSocket = this.shimServerSocket;
799             if (shimServerSocket != null) {
800                 try {
801                     shimServerSocket.close();
802                 } catch (IOException e) {
803                     logger.debug("Error closing GoogleTV SSL socket: {}", e.getMessage());
804                 }
805                 this.shimServerSocket = null;
806             }
807             BufferedReader shimReader = this.shimReader;
808             if (shimReader != null) {
809                 try {
810                     shimReader.close();
811                 } catch (IOException e) {
812                     logger.debug("Error closing shimReader: {}", e.getMessage());
813                 }
814             }
815             BufferedWriter shimWriter = this.shimWriter;
816             if (shimWriter != null) {
817                 try {
818                     shimWriter.close();
819                 } catch (IOException e) {
820                     logger.debug("Error closing shimWriter: {}", e.getMessage());
821                 }
822             }
823         }
824     }
825
826     private void reconnect() {
827         synchronized (connectionLock) {
828             if (!this.disposing) {
829                 logger.debug("{} - Attempting to reconnect to the GoogleTV", handler.getThingID());
830                 setStatus(false, "offline.reconnecting");
831                 disconnect(false);
832                 connect();
833             }
834         }
835     }
836
837     /**
838      * Method executed by the message sender thread (senderThread)
839      */
840     private void senderThreadJob() {
841         logger.debug("{} - Command sender thread started {}", handler.getThingID(), config.port);
842         try {
843             while (!Thread.currentThread().isInterrupted() && writer != null) {
844                 GoogleTVCommand command = sendQueue.take();
845
846                 try {
847                     BufferedWriter writer = this.writer;
848                     if (writer != null) {
849                         logger.trace("{} - Raw GoogleTV command decodes as: {}", handler.getThingID(),
850                                 GoogleTVRequest.decodeMessage(command.toString()));
851                         writer.write(command.toString());
852                         writer.flush();
853                     }
854                 } catch (InterruptedIOException e) {
855                     logger.debug("Interrupted while sending to GoogleTV");
856                     setStatus(false, "offline.interrupted");
857                     break; // exit loop and terminate thread
858                 } catch (IOException e) {
859                     logger.warn("{} - Communication error, will try to reconnect GoogleTV. Error: {}",
860                             handler.getThingID(), e.getMessage());
861                     setStatus(false, "offline.communication-error-will-try-to-reconnect");
862                     sendQueue.add(command); // Requeue command
863                     this.isLoggedIn = false;
864                     reconnect();
865                     break; // reconnect() will start a new thread; terminate this one
866                 }
867                 if (config.delay > 0) {
868                     Thread.sleep(config.delay); // introduce delay to throttle send rate
869                 }
870             }
871         } catch (InterruptedException e) {
872             Thread.currentThread().interrupt();
873         } finally {
874             logger.debug("{} - Command sender thread exiting {}", handler.getThingID(), config.port);
875         }
876     }
877
878     private void shimSenderThreadJob() {
879         logger.debug("Shim sender thread started");
880         try {
881             while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
882                 GoogleTVCommand command = shimQueue.take();
883
884                 try {
885                     BufferedWriter writer = this.shimWriter;
886                     if (writer != null) {
887                         logger.trace("Shim received from google: {}",
888                                 GoogleTVRequest.decodeMessage(command.toString()));
889                         writer.write(command.toString());
890                         writer.flush();
891                     }
892                 } catch (InterruptedIOException e) {
893                     logger.debug("Shim interrupted while sending.");
894                     break; // exit loop and terminate thread
895                 } catch (IOException e) {
896                     logger.warn("Shim communication error. Error: {}", e.getMessage());
897                     break; // reconnect() will start a new thread; terminate this one
898                 }
899             }
900         } catch (InterruptedException e) {
901             Thread.currentThread().interrupt();
902         } finally {
903             logger.debug("Command sender thread exiting");
904         }
905     }
906
907     /**
908      * Method executed by the message reader thread (readerThread)
909      */
910     private void readerThreadJob() {
911         logger.debug("{} - Message reader thread started {}", handler.getThingID(), config.port);
912         try {
913             BufferedReader reader = this.reader;
914             int length = 0;
915             int current = 0;
916             while (!Thread.interrupted() && reader != null) {
917                 thisMsg = GoogleTVRequest.fixMessage(Integer.toHexString(reader.read()));
918                 if (HARD_DROP.equals(thisMsg)) {
919                     // Google has crashed the connection. Disconnect hard.
920                     logger.debug("{} - readerThreadJob received ffffffff.  Disconnecting hard.", handler.getThingID());
921                     this.isLoggedIn = false;
922                     reconnect();
923                     break;
924                 }
925                 if (length == 0) {
926                     length = Integer.parseInt(thisMsg.toString(), 16);
927                     logger.trace("{} - readerThreadJob message length {}", handler.getThingID(), length);
928                     current = 0;
929                     sbReader = new StringBuffer();
930                     sbReader.append(thisMsg.toString());
931                 } else {
932                     sbReader.append(thisMsg.toString());
933                     current += 1;
934                 }
935
936                 if ((length > 0) && (current == length)) {
937                     logger.trace("{} - GoogleTV Message: {} {}", handler.getThingID(), length, sbReader.toString());
938                     messageParser.handleMessage(sbReader.toString());
939                     if (config.shim) {
940                         String thisCommand = interceptMessages(sbReader.toString());
941                         shimQueue.add(new GoogleTVCommand(GoogleTVRequest.encodeMessage(thisCommand)));
942                     }
943                     length = 0;
944                 }
945             }
946         } catch (InterruptedIOException e) {
947             logger.debug("Interrupted while reading");
948             setStatus(false, "offline.interrupted");
949         } catch (IOException e) {
950             String message = e.getMessage();
951             if ((message != null) && (message.contains("certificate_unknown")) && (!config.mode.equals(PIN_MODE))
952                     && (!config.shim)) {
953                 setStatus(false, "offline.pin-process-incomplete");
954                 logger.debug("{} - GoogleTV PIN Process Incomplete", handler.getThingID());
955                 reconnectTaskCancel(true);
956                 startChildConnectionManager(this.config.port + 1, PIN_MODE);
957             } else if ((message != null) && (message.contains("certificate_unknown")) && (config.shim)) {
958                 logger.debug("Shim cert_unknown I/O error while reading from stream: {}", e.getMessage());
959                 Socket shimServerSocket = this.shimServerSocket;
960                 if (shimServerSocket != null) {
961                     try {
962                         shimServerSocket.close();
963                     } catch (IOException ex) {
964                         logger.debug("Error closing GoogleTV SSL socket: {}", ex.getMessage());
965                     }
966                     this.shimServerSocket = null;
967                 }
968             } else {
969                 logger.debug("I/O error while reading from stream: {}", e.getMessage());
970                 setStatus(false, "offline.io-error");
971             }
972         } catch (RuntimeException e) {
973             logger.warn("Runtime exception in reader thread", e);
974             setStatus(false, "offline.runtime-exception");
975         } finally {
976             logger.debug("{} - Message reader thread exiting {}", handler.getThingID(), config.port);
977         }
978     }
979
980     private String interceptMessages(String message) {
981         if (message.startsWith("080210c801c202", 2)) {
982             // intercept PIN hash and replace with valid shim hash
983             int length = this.pinHash.length() / 2;
984             String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
985             String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
986             String reply = "080210c801c202" + len1 + "0a" + len2 + this.pinHash;
987             String replyLength = GoogleTVRequest.fixMessage(Integer.toHexString(reply.length() / 2));
988             String finalReply = replyLength + reply;
989             logger.trace("Message Intercepted: {}", message);
990             logger.trace("Message chagnged to: {}", finalReply);
991             return finalReply;
992         } else if (message.startsWith("080210c801ca02", 2)) {
993             // intercept PIN hash and replace with valid shim hash
994             int length = this.shimPinHash.length() / 2;
995             String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
996             String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
997             String reply = "080210c801ca02" + len1 + "0a" + len2 + this.shimPinHash;
998             String replyLength = GoogleTVRequest.fixMessage(Integer.toHexString(reply.length() / 2));
999             String finalReply = replyLength + reply;
1000             logger.trace("Message Intercepted: {}", message);
1001             logger.trace("Message chagnged to: {}", finalReply);
1002             return finalReply;
1003         } else {
1004             // don't intercept message
1005             return message;
1006         }
1007     }
1008
1009     private void shimReaderThreadJob() {
1010         logger.debug("Shim reader thread started {}", config.port);
1011         try {
1012             BufferedReader reader = this.shimReader;
1013             String thisShimMsg = "";
1014             int length = 0;
1015             int current = 0;
1016             while (!Thread.interrupted() && reader != null) {
1017                 thisShimMsg = GoogleTVRequest.fixMessage(Integer.toHexString(reader.read()));
1018                 if (HARD_DROP.equals(thisShimMsg)) {
1019                     // Google has crashed the connection. Disconnect hard.
1020                     disconnect(false);
1021                     break;
1022                 }
1023                 if (length == 0) {
1024                     length = Integer.parseInt(thisShimMsg.toString(), 16);
1025                     logger.trace("shimReaderThreadJob message length {}", length);
1026                     current = 0;
1027                     sbShimReader = new StringBuffer();
1028                     sbShimReader.append(thisShimMsg.toString());
1029                 } else {
1030                     sbShimReader.append(thisShimMsg.toString());
1031                     current += 1;
1032                 }
1033                 if ((length > 0) && (current == length)) {
1034                     logger.trace("Shim GoogleTV Message: {} {}", length, sbShimReader.toString());
1035                     String thisCommand = interceptMessages(sbShimReader.toString());
1036                     sendQueue.add(new GoogleTVCommand(GoogleTVRequest.encodeMessage(thisCommand)));
1037                     length = 0;
1038                 }
1039             }
1040         } catch (InterruptedIOException e) {
1041             logger.debug("Interrupted while reading");
1042             setStatus(false, "offline.interrupted");
1043         } catch (IOException e) {
1044             logger.debug("I/O error while reading from stream: {}", e.getMessage());
1045             setStatus(false, "offline.io-error");
1046         } catch (RuntimeException e) {
1047             logger.warn("Runtime exception in reader thread", e);
1048             setStatus(false, "offline.runtime-exception");
1049         } finally {
1050             logger.debug("Shim message reader thread exiting {}", config.port);
1051         }
1052     }
1053
1054     public void sendKeepAlive(String request) {
1055         String keepalive = GoogleTVRequest.encodeMessage(GoogleTVRequest.keepAlive(request));
1056         logger.debug("{} - Sending GoogleTV keepalive - request {} - response {}", handler.getThingID(), request,
1057                 GoogleTVRequest.decodeMessage(keepalive));
1058         sendCommand(new GoogleTVCommand(keepalive));
1059         reconnectTaskSchedule();
1060     }
1061
1062     /**
1063      * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
1064      * be
1065      * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
1066      */
1067     private void reconnectTaskSchedule() {
1068         synchronized (keepAliveReconnectLock) {
1069             logger.trace("{} - Scheduling Reconnect Job for {}", handler.getThingID(), KEEPALIVE_TIMEOUT_SECONDS);
1070             keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
1071                     TimeUnit.SECONDS);
1072         }
1073     }
1074
1075     /**
1076      * Cancels the reconnect task keepAliveReconnectJob.
1077      */
1078     private void reconnectTaskCancel(boolean interrupt) {
1079         synchronized (keepAliveReconnectLock) {
1080             ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
1081             if (keepAliveReconnectJob != null) {
1082                 logger.trace("{} - Canceling GoogleTV scheduled reconnect job.", handler.getThingID());
1083                 keepAliveReconnectJob.cancel(interrupt);
1084                 this.keepAliveReconnectJob = null;
1085             }
1086         }
1087     }
1088
1089     /**
1090      * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
1091      * validMessageReceived() which in turn calls reconnectTaskCancel().
1092      */
1093     private void keepAliveTimeoutExpired() {
1094         logger.debug("{} - GoogleTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
1095         reconnect();
1096     }
1097
1098     public void validMessageReceived() {
1099         reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
1100     }
1101
1102     public void finishPinProcess() {
1103         GoogleTVConnectionManager connectionManager = this.connectionManager;
1104         GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1105         if ((connectionManager != null) && (config.mode.equals(PIN_MODE)) && (!config.shim)) {
1106             disconnect(false);
1107             connectionManager.finishPinProcess();
1108         } else if ((childConnectionManager != null) && (config.mode.equals(DEFAULT_MODE)) && (!config.shim)) {
1109             childConnectionManager.dispose();
1110             reconnect();
1111         }
1112     }
1113
1114     public void sendCommand(GoogleTVCommand command) {
1115         if ((!config.shim) && (!command.isEmpty())) {
1116             int length = command.toString().length();
1117             String hexLength = GoogleTVRequest.encodeMessage(GoogleTVRequest.fixMessage(Integer.toHexString(length)));
1118             String message = hexLength + command.toString();
1119             GoogleTVCommand lenCommand = new GoogleTVCommand(message);
1120             sendQueue.add(lenCommand);
1121         }
1122     }
1123
1124     public void sendShim(GoogleTVCommand command) {
1125         if (!command.isEmpty()) {
1126             shimQueue.add(command);
1127         }
1128     }
1129
1130     public void handleCommand(ChannelUID channelUID, Command command) {
1131         logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
1132
1133         if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
1134             if (command instanceof StringType) {
1135                 if (command.toString().length() == 5) {
1136                     // Account for KEY_(ASCII Character)
1137                     String keyPress = "aa01071a0512031a01"
1138                             + GoogleTVRequest.decodeMessage(new String("" + command.toString().charAt(4)));
1139                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(keyPress)));
1140                     return;
1141                 }
1142
1143                 String message = "";
1144                 String suffix = "";
1145                 String shortCommand = command.toString();
1146                 if (command.toString().endsWith("_PRESS")) {
1147                     suffix = "1001";
1148                     shortCommand = "KEY_" + command.toString().split("_")[1];
1149                 } else if (command.toString().endsWith("_RELEASE")) {
1150                     suffix = "1002";
1151                     shortCommand = "KEY_" + command.toString().split("_")[1];
1152                 } else {
1153                     suffix = "1003";
1154                 }
1155
1156                 switch (shortCommand) {
1157                     case "KEY_UP":
1158                         message = "52040813" + suffix;
1159                         break;
1160                     case "KEY_DOWN":
1161                         message = "52040814" + suffix;
1162                         break;
1163                     case "KEY_RIGHT":
1164                         message = "52040816" + suffix;
1165                         break;
1166                     case "KEY_LEFT":
1167                         message = "52040815" + suffix;
1168                         break;
1169                     case "KEY_ENTER":
1170                         message = "52040817" + suffix;
1171                         break;
1172                     case "KEY_HOME":
1173                         message = "52040803" + suffix;
1174                         break;
1175                     case "KEY_BACK":
1176                         message = "52040804" + suffix;
1177                         break;
1178                     case "KEY_MENU":
1179                         message = "52040852" + suffix;
1180                         break;
1181                     case "KEY_PLAY":
1182                         message = "5204087E" + suffix;
1183                         break;
1184                     case "KEY_PAUSE":
1185                         message = "5204087F" + suffix;
1186                         break;
1187                     case "KEY_PLAYPAUSE":
1188                         message = "52040855" + suffix;
1189                         break;
1190                     case "KEY_STOP":
1191                         message = "52040856" + suffix;
1192                         break;
1193                     case "KEY_NEXT":
1194                         message = "52040857" + suffix;
1195                         break;
1196                     case "KEY_PREVIOUS":
1197                         message = "52040858" + suffix;
1198                         break;
1199                     case "KEY_REWIND":
1200                         message = "52040859" + suffix;
1201                         break;
1202                     case "KEY_FORWARD":
1203                         message = "5204085A" + suffix;
1204                         break;
1205                     case "KEY_POWER":
1206                         message = "5204081a" + suffix;
1207                         break;
1208                     case "KEY_VOLUP":
1209                         message = "52040818" + suffix;
1210                         break;
1211                     case "KEY_VOLDOWN":
1212                         message = "52040819" + suffix;
1213                         break;
1214                     case "KEY_MUTE":
1215                         message = "5204085b" + suffix;
1216                         break;
1217                     default:
1218                         logger.debug("Unknown Key {}", command);
1219                         return;
1220                 }
1221                 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1222             }
1223         } else if (CHANNEL_KEYCODE.equals(channelUID.getId())) {
1224             if (command instanceof StringType) {
1225                 String shortCommand = command.toString().split("_")[0];
1226                 int commandInt = Integer.parseInt(shortCommand, 10);
1227                 String suffix = "";
1228                 if (commandInt > 255) {
1229                     suffix = "02";
1230                     commandInt -= 256;
1231                 } else if (commandInt > 127) {
1232                     suffix = "01";
1233                 }
1234
1235                 String key = Integer.toHexString(commandInt) + suffix;
1236
1237                 if ((key.length() % 2) == 1) {
1238                     key = "0" + key;
1239                 }
1240
1241                 key = "08" + key;
1242
1243                 if (command.toString().endsWith("_PRESS")) {
1244                     key = key + "1001";
1245                 } else if (command.toString().endsWith("_RELEASE")) {
1246                     key = key + "1002";
1247                 } else {
1248                     key = key + "1003";
1249                 }
1250
1251                 String length = "0" + (key.length() / 2);
1252                 String message = "52" + length + key;
1253
1254                 logger.trace("Sending KEYCODE {} as {}", key, message);
1255                 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1256             }
1257
1258         } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1259             if (command instanceof StringType) {
1260                 try {
1261                     Certificate[] shimClientChain = this.shimClientChain;
1262                     Certificate[] shimServerChain = this.shimServerChain;
1263                     Certificate[] shimClientLocalChain = this.shimClientLocalChain;
1264                     if (config.mode.equals(DEFAULT_MODE)) {
1265                         if ((!isLoggedIn) && (command.toString().equals("REQUEST"))
1266                                 && (childConnectionManager == null)) {
1267                             setStatus(false, "offline.user-forced-pin-process");
1268                             logger.debug("{} - User Forced PIN Process", handler.getThingID());
1269                             disconnect(true);
1270                             startChildConnectionManager(config.port + 1, PIN_MODE);
1271                             try {
1272                                 Thread.sleep(PIN_DELAY);
1273                             } catch (InterruptedException e) {
1274                                 logger.trace("InterruptedException", e);
1275                             }
1276                         }
1277                         GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1278                         if (childConnectionManager != null) {
1279                             childConnectionManager.handleCommand(channelUID, command);
1280                         } else {
1281                             logger.debug("{} - Child Connection Manager unavailable.", handler.getThingID());
1282                         }
1283                     } else if ((config.mode.equals(PIN_MODE)) && (!config.shim)) {
1284                         if (!isLoggedIn) {
1285                             if (command.toString().equals("REQUEST")) {
1286                                 sendCommand(new GoogleTVCommand(
1287                                         GoogleTVRequest.encodeMessage(GoogleTVRequest.pinRequest(command.toString()))));
1288                             } else if (shimServerChain != null) {
1289                                 this.pinHash = GoogleTVUtils.validatePIN(command.toString(), androidtvPKI.getCert(),
1290                                         shimServerChain[0]);
1291                                 sendCommand(new GoogleTVCommand(
1292                                         GoogleTVRequest.encodeMessage(GoogleTVRequest.pinRequest(this.pinHash))));
1293                             }
1294                         }
1295                     } else if ((config.mode.equals(PIN_MODE)) && (config.shim)) {
1296                         if ((shimClientChain != null) && (shimServerChain != null) && (shimClientLocalChain != null)) {
1297                             this.pinHash = GoogleTVUtils.validatePIN(command.toString(), androidtvPKI.getCert(),
1298                                     shimServerChain[0]);
1299                             this.shimPinHash = GoogleTVUtils.validatePIN(command.toString(), shimClientChain[0],
1300                                     shimClientLocalChain[0]);
1301                         }
1302                     }
1303                 } catch (CertificateException e) {
1304                     logger.trace("PIN CertificateException", e);
1305                 }
1306             }
1307         } else if (CHANNEL_POWER.equals(channelUID.getId())) {
1308             if (command instanceof OnOffType) {
1309                 if ((power && command.equals(OnOffType.OFF)) || (!power && command.equals(OnOffType.ON))) {
1310                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204081a1003")));
1311                 }
1312             } else if (command instanceof StringType) {
1313                 if ((power && command.toString().equals("OFF")) || (!power && command.toString().equals("ON"))) {
1314                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204081a1003")));
1315                 }
1316             }
1317         } else if (CHANNEL_MUTE.equals(channelUID.getId())) {
1318             if (command instanceof OnOffType) {
1319                 if ((volMute && command.equals(OnOffType.OFF)) || (!volMute && command.equals(OnOffType.ON))) {
1320                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204085b1003")));
1321                 }
1322             }
1323         } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1324             if (command instanceof StringType) {
1325                 if (command.toString().startsWith("RAW", 9)) {
1326                     String newCommand = command.toString().substring(13);
1327                     String message = GoogleTVRequest.encodeMessage(newCommand);
1328                     if (logger.isTraceEnabled()) {
1329                         logger.trace("Raw Message Decodes as: {}", GoogleTVRequest.decodeMessage(message));
1330                     }
1331                     sendCommand(new GoogleTVCommand(message));
1332                 } else if (command.toString().startsWith("MSG", 9)) {
1333                     String newCommand = command.toString().substring(13);
1334                     messageParser.handleMessage(newCommand);
1335                 }
1336             }
1337         } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1338             if (command instanceof StringType) {
1339                 String keyPress = "";
1340                 for (int i = 0; i < command.toString().length(); i++) {
1341                     keyPress = "aa01071a0512031a01"
1342                             + GoogleTVRequest.decodeMessage(String.valueOf(command.toString().charAt(i)));
1343                     sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(keyPress)));
1344                 }
1345             }
1346         } else if (CHANNEL_PLAYER.equals(channelUID.getId())) {
1347             String message = "";
1348             if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
1349                 message = "5204087F1003";
1350             } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
1351                 message = "5204087E1003";
1352             } else if (command == NextPreviousType.NEXT) {
1353                 message = "520408571003";
1354             } else if (command == NextPreviousType.PREVIOUS) {
1355                 message = "520408581003";
1356             } else if (command == RewindFastforwardType.FASTFORWARD) {
1357                 message = "5204085A1003";
1358             } else if (command == RewindFastforwardType.REWIND) {
1359                 message = "520408591003";
1360             }
1361             sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1362         }
1363     }
1364
1365     public void dispose() {
1366         this.disposing = true;
1367
1368         Future<?> asyncInitializeTask = this.asyncInitializeTask;
1369         if (asyncInitializeTask != null) {
1370             asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1371         }
1372         Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1373         if (shimAsyncInitializeTask != null) {
1374             shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1375         }
1376         ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1377         if (deviceHealthJob != null) {
1378             deviceHealthJob.cancel(true);
1379         }
1380         GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1381         if (childConnectionManager != null) {
1382             childConnectionManager.dispose();
1383         }
1384         disconnect(true);
1385     }
1386 }