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