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