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