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