2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.androidtv.internal.protocol.googletv;
15 import static org.openhab.binding.androidtv.internal.AndroidTVBindingConstants.*;
16 import static org.openhab.binding.androidtv.internal.protocol.googletv.GoogleTVConstants.*;
18 import java.io.BufferedReader;
19 import java.io.BufferedWriter;
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;
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;
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;
75 * The {@link GoogleTVConnectionManager} is responsible for handling connections via the googletv protocol
77 * Significant portions reused from Lutron binding with permission from Bob A.
79 * @author Ben Rosenblum - Initial contribution
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;
92 private final Logger logger = LoggerFactory.getLogger(GoogleTVConnectionManager.class);
94 private ScheduledExecutorService scheduler;
96 private final AndroidTVHandler handler;
97 private GoogleTVConfiguration config;
98 private final AndroidTVTranslationProvider translationProvider;
100 private @NonNullByDefault({}) SSLSocketFactory sslSocketFactory;
101 private @Nullable SSLSocket sslSocket;
102 private @Nullable BufferedWriter writer;
103 private @Nullable BufferedReader reader;
105 private @NonNullByDefault({}) SSLServerSocketFactory sslServerSocketFactory;
106 private @Nullable Socket shimServerSocket;
107 private @Nullable BufferedWriter shimWriter;
108 private @Nullable BufferedReader shimReader;
110 private @Nullable GoogleTVConnectionManager connectionManager;
111 private @Nullable GoogleTVConnectionManager childConnectionManager;
112 private @NonNullByDefault({}) GoogleTVMessageParser messageParser;
114 private final BlockingQueue<GoogleTVCommand> sendQueue = new LinkedBlockingQueue<>();
115 private final BlockingQueue<GoogleTVCommand> shimQueue = new LinkedBlockingQueue<>();
117 private @Nullable Future<?> asyncInitializeTask;
118 private @Nullable Future<?> shimAsyncInitializeTask;
120 private @Nullable Thread senderThread;
121 private @Nullable Thread readerThread;
122 private @Nullable Thread shimSenderThread;
123 private @Nullable Thread shimReaderThread;
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();
131 private @Nullable ScheduledFuture<?> deviceHealthJob;
132 private boolean isOnline = true;
134 private StringBuffer sbReader = new StringBuffer();
135 private StringBuffer sbShimReader = new StringBuffer();
136 private String thisMsg = "";
138 private X509Certificate @Nullable [] shimX509ClientChain;
139 private Certificate @Nullable [] shimClientChain;
140 private Certificate @Nullable [] shimServerChain;
141 private Certificate @Nullable [] shimClientLocalChain;
143 private boolean disposing = false;
144 private boolean isLoggedIn = false;
145 private String statusMessage = "";
146 private String pinHash = "";
147 private String shimPinHash = "";
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 = "";
161 private AndroidTVPKI androidtvPKI = new AndroidTVPKI();
162 private byte[] encryptionKey;
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();
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();
187 public AndroidTVHandler getHandler() {
191 public String getThingID() {
192 return handler.getThingID();
195 public void setManufacturer(String manufacturer) {
196 this.manufacturer = manufacturer;
197 handler.setThingProperty("manufacturer", manufacturer);
200 public String getManufacturer() {
204 public void setModel(String model) {
206 handler.setThingProperty("model", model);
209 public String getModel() {
213 public void setAndroidVersion(String androidVersion) {
214 this.androidVersion = androidVersion;
215 handler.setThingProperty("androidVersion", androidVersion);
218 public String getAndroidVersion() {
219 return androidVersion;
222 public void setRemoteServer(String remoteServer) {
223 this.remoteServer = remoteServer;
224 handler.setThingProperty("remoteServer", remoteServer);
227 public String getRemoteServer() {
231 public void setRemoteServerVersion(String remoteServerVersion) {
232 this.remoteServerVersion = remoteServerVersion;
233 handler.setThingProperty("remoteServerVersion", remoteServerVersion);
236 public String getRemoteServerVersion() {
237 return remoteServerVersion;
240 public void setPower(boolean power) {
242 logger.debug("{} - Setting power to {}", handler.getThingID(), power);
244 handler.updateChannelState(CHANNEL_POWER, OnOffType.ON);
246 handler.updateChannelState(CHANNEL_POWER, OnOffType.OFF);
250 public boolean getPower() {
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));
261 public String getVolCurr() {
265 public void setVolMax(String volMax) {
266 this.volMax = volMax;
269 public String getVolMax() {
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)) {
279 handler.updateChannelState(CHANNEL_MUTE, OnOffType.ON);
283 public boolean getVolMute() {
287 public void setAudioMode(String audioMode) {
288 this.audioMode = audioMode;
291 public String getAudioMode() {
295 public void setCurrentApp(String currentApp) {
296 this.currentApp = currentApp;
297 handler.updateChannelState(CHANNEL_APP, new StringType(currentApp));
300 public String getStatusMessage() {
301 return statusMessage;
304 private void setStatus(boolean isLoggedIn) {
306 setStatus(isLoggedIn, "online.online");
308 setStatus(isLoggedIn, "offline.unknown");
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();
321 public String getCurrentApp() {
325 public void setLoggedIn(boolean isLoggedIn) {
326 if (this.isLoggedIn != isLoggedIn) {
327 setStatus(isLoggedIn);
331 public boolean getLoggedIn() {
335 private boolean servicePing() {
338 SocketAddress socketAddress = new InetSocketAddress(config.ipAddress, config.port);
339 try (Socket socket = new Socket()) {
340 socket.connect(socketAddress, timeout);
342 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
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
351 private void checkHealth() {
354 isOnline = servicePing();
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;
363 logger.debug("{} - Device is back online. Attempting reconnection.", handler.getThingID());
369 private void setShimX509ClientChain(X509Certificate @Nullable [] shimX509ClientChain) {
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())));
381 androidtvPKI.setCaCert(shimX509ClientChain[0]);
382 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
385 } catch (Exception e) {
386 logger.trace("setShimX509ClientChain Exception", e);
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);
408 private TrustManager[] defineNoOpTrustManager() {
409 return new TrustManager[] { new X509TrustManager() {
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());
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());
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());
444 return x509ClientChain;
446 logger.debug("Returning empty certificate for getAcceptedIssuers");
447 return new X509Certificate[0];
453 private void initialize() {
454 SSLContext sslContext;
456 String folderName = OpenHAB.getUserDataFolder() + "/androidtv";
457 File folder = new File(folderName);
459 if (!folder.exists()) {
460 logger.debug("Creating directory {}", folderName);
464 config.port = (config.port > 0) ? config.port : DEFAULT_PORT;
465 config.reconnect = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_SECONDS;
466 config.heartbeat = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_SECONDS;
467 config.delay = (config.delay < 0) ? 0 : config.delay;
468 config.shim = (config.shim) ? true : false;
469 config.shimNewKeys = (config.shimNewKeys) ? true : false;
470 config.mode = (!config.mode.equals("")) ? config.mode : DEFAULT_MODE;
472 config.keystoreFileName = (!config.keystoreFileName.equals("")) ? config.keystoreFileName
473 : folderName + "/googletv." + ((config.shim) ? "shim." : "") + handler.getThing().getUID().getId()
475 config.keystorePassword = (!config.keystorePassword.equals("")) ? config.keystorePassword
476 : DEFAULT_KEYSTORE_PASSWORD;
478 androidtvPKI.setKeystoreFileName(config.keystoreFileName);
479 androidtvPKI.setAlias("nvidia");
481 if (config.mode.equals(DEFAULT_MODE)) {
482 deviceHealthJob = scheduler.scheduleWithFixedDelay(this::checkHealth, config.heartbeat, config.heartbeat,
487 File keystoreFile = new File(config.keystoreFileName);
489 if (!keystoreFile.exists() || config.shimNewKeys) {
490 androidtvPKI.generateNewKeyPair(encryptionKey);
491 androidtvPKI.saveKeyStore(config.keystorePassword, this.encryptionKey);
493 androidtvPKI.loadFromKeyStore(config.keystorePassword, this.encryptionKey);
496 logger.trace("{} - Initializing SSL Context", handler.getThingID());
497 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
498 kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
499 config.keystorePassword.toCharArray());
501 TrustManager[] trustManagers = defineNoOpTrustManager();
503 sslContext = SSLContext.getInstance("TLS");
504 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
506 sslSocketFactory = sslContext.getSocketFactory();
508 asyncInitializeTask = scheduler.submit(this::connect);
510 shimAsyncInitializeTask = scheduler.submit(this::shimInitialize);
512 } catch (NoSuchAlgorithmException | IOException e) {
513 setStatus(false, "offline.error-initalizing-keystore");
514 logger.debug("Error initializing keystore", e);
515 } catch (UnrecoverableKeyException e) {
516 setStatus(false, "offline.key-unrecoverable-with-supplied-password");
517 } catch (GeneralSecurityException e) {
518 logger.debug("General security exception", e);
519 } catch (Exception e) {
520 logger.debug("General exception", e);
524 public void connect() {
525 synchronized (connectionLock) {
526 if (isOnline || config.mode.equals(PIN_MODE)) {
528 logger.debug("{} - Opening GoogleTV SSL connection to {}:{} {}", handler.getThingID(),
529 config.ipAddress, config.port, config.mode);
530 SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(config.ipAddress, config.port);
531 sslSocket.startHandshake();
532 this.shimServerChain = ((SSLSocket) sslSocket).getSession().getPeerCertificates();
533 writer = new BufferedWriter(
534 new OutputStreamWriter(sslSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
535 reader = new BufferedReader(
536 new InputStreamReader(sslSocket.getInputStream(), StandardCharsets.ISO_8859_1));
537 this.sslSocket = sslSocket;
538 this.sendQueue.clear();
539 logger.debug("{} - Connection to {}:{} {} successful", handler.getThingID(), config.ipAddress,
540 config.port, config.mode);
541 } catch (UnknownHostException e) {
542 setStatus(false, "offline.unknown-host");
543 logger.debug("{} - Unknown host {}", handler.getThingID(), config.ipAddress);
545 } catch (IllegalArgumentException e) {
546 // port out of valid range
547 setStatus(false, "offline.invalid-port-number");
548 logger.debug("{} - Invalid port number {}:{}", handler.getThingID(), config.ipAddress, config.port);
550 } catch (InterruptedIOException e) {
551 logger.debug("{} - Interrupted while establishing GoogleTV connection", handler.getThingID());
552 Thread.currentThread().interrupt();
554 } catch (IOException e) {
555 String message = e.getMessage();
556 if ((message != null) && (message.contains("certificate_unknown"))
557 && (!config.mode.equals(PIN_MODE)) && (!config.shim)) {
558 setStatus(false, "offline.pin-process-incomplete");
559 logger.debug("{} - GoogleTV PIN Process Incomplete", handler.getThingID());
560 reconnectTaskCancel(true);
561 startChildConnectionManager(this.config.port + 1, PIN_MODE);
562 } else if ((message != null) && (message.contains("certificate_unknown")) && (config.shim)) {
563 logger.debug("Shim cert_unknown I/O error while connecting: {}", e.getMessage());
564 Socket shimServerSocket = this.shimServerSocket;
565 if (shimServerSocket != null) {
567 shimServerSocket.close();
568 } catch (IOException ex) {
569 logger.debug("Error closing GoogleTV SSL socket: {}", ex.getMessage());
571 this.shimServerSocket = null;
574 setStatus(false, "offline.error-opening-ssl-connection-check-log");
575 logger.info("{} - Error opening SSL connection to {}:{} {}", handler.getThingID(),
576 config.ipAddress, config.port, e.getMessage());
578 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
583 setStatus(false, "offline.initializing");
585 logger.trace("{} - Starting Reader Thread for {}:{}", handler.getThingID(), config.ipAddress,
588 Thread readerThread = new Thread(this::readerThreadJob, "GoogleTV reader " + handler.getThingID());
589 readerThread.setDaemon(true);
590 readerThread.start();
591 this.readerThread = readerThread;
593 logger.trace("{} - Starting Sender Thread for {}:{}", handler.getThingID(), config.ipAddress,
596 Thread senderThread = new Thread(this::senderThreadJob, "GoogleTV sender " + handler.getThingID());
597 senderThread.setDaemon(true);
598 senderThread.start();
599 this.senderThread = senderThread;
601 logger.trace("{} - Checking for PIN MODE for {}:{} {}", handler.getThingID(), config.ipAddress,
602 config.port, config.mode);
604 if (config.mode.equals(PIN_MODE)) {
605 logger.trace("{} - Sending PIN Login to {}:{}", handler.getThingID(), config.ipAddress,
607 // Send app name and device name
608 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(1))));
609 // Unknown but required
610 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(GoogleTVRequest.loginRequest(2))));
611 // Don't send pin request yet, let user send REQUEST via PINCODE channel
613 logger.trace("{} - Not PIN Mode {}:{} {}", handler.getThingID(), config.ipAddress, config.port,
617 scheduleConnectRetry(config.reconnect); // Possibly a temporary problem. Try again later.
622 public void shimInitialize() {
623 synchronized (connectionLock) {
624 SSLContext sslContext;
627 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
628 kmf.init(androidtvPKI.getKeyStore(config.keystorePassword, this.encryptionKey),
629 config.keystorePassword.toCharArray());
631 TrustManager[] trustManagers = defineNoOpTrustManager();
633 sslContext = SSLContext.getInstance("TLS");
634 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
635 this.sslServerSocketFactory = sslContext.getServerSocketFactory();
637 logger.trace("Opening GoogleTV shim on port {}", config.port);
638 SSLServerSocket sslServerSocket = (SSLServerSocket) this.sslServerSocketFactory
639 .createServerSocket(config.port);
640 if (this.config.mode.equals(DEFAULT_MODE)) {
641 sslServerSocket.setNeedClientAuth(true);
643 sslServerSocket.setWantClientAuth(true);
647 logger.trace("Waiting for shim connection... {}", config.port);
648 if (this.config.mode.equals(DEFAULT_MODE) && (childConnectionManager == null)) {
649 logger.trace("Starting childConnectionManager {}", config.port);
650 startChildConnectionManager(this.config.port + 1, PIN_MODE);
652 SSLSocket serverSocket = (SSLSocket) sslServerSocket.accept();
653 logger.trace("shimInitialize accepted {}", config.port);
655 serverSocket.startHandshake();
656 logger.trace("shimInitialize startHandshake {}", config.port);
658 logger.trace("shimInitialize connected {}", config.port);
660 SSLSession session = serverSocket.getSession();
661 Certificate[] cchain2 = session.getPeerCertificates();
662 this.shimClientChain = cchain2;
663 Certificate[] cchain3 = session.getLocalCertificates();
664 this.shimClientLocalChain = cchain3;
666 X509Certificate[] shimX509ClientChain = new X509Certificate[cchain2.length];
668 for (int i = 0; i < cchain2.length; i++) {
669 logger.trace("Connection from: {}",
670 ((X509Certificate) cchain2[i]).getSubjectX500Principal());
671 shimX509ClientChain[i] = ((X509Certificate) cchain2[i]);
672 if (this.config.mode.equals(DEFAULT_MODE) && logger.isTraceEnabled()) {
673 logger.trace("Cert: {}", GoogleTVRequest.decodeMessage(
674 GoogleTVUtils.byteArrayToString(((X509Certificate) cchain2[i]).getEncoded())));
678 if (this.config.mode.equals(PIN_MODE)) {
679 this.shimX509ClientChain = shimX509ClientChain;
680 GoogleTVConnectionManager connectionManager = this.connectionManager;
681 if (connectionManager != null) {
682 connectionManager.setShimX509ClientChain(shimX509ClientChain);
686 if (cchain3 != null) {
687 for (int i = 0; i < cchain3.length; i++) {
688 logger.trace("Connection from: {}",
689 ((X509Certificate) cchain3[i]).getSubjectX500Principal());
693 logger.trace("Peer host is {}", session.getPeerHost());
694 logger.trace("Cipher is {}", session.getCipherSuite());
695 logger.trace("Protocol is {}", session.getProtocol());
696 logger.trace("ID is {}", new BigInteger(session.getId()));
697 logger.trace("Session created in {}", session.getCreationTime());
698 logger.trace("Session accessed in {}", session.getLastAccessedTime());
700 shimWriter = new BufferedWriter(
701 new OutputStreamWriter(serverSocket.getOutputStream(), StandardCharsets.ISO_8859_1));
702 shimReader = new BufferedReader(
703 new InputStreamReader(serverSocket.getInputStream(), StandardCharsets.ISO_8859_1));
704 this.shimServerSocket = serverSocket;
705 this.shimQueue.clear();
707 Thread readerThread = new Thread(this::shimReaderThreadJob, "GoogleTV shim reader");
708 readerThread.setDaemon(true);
709 readerThread.start();
710 this.shimReaderThread = readerThread;
712 Thread senderThread = new Thread(this::shimSenderThreadJob, "GoogleTV shim sender");
713 senderThread.setDaemon(true);
714 senderThread.start();
715 this.shimSenderThread = senderThread;
716 } catch (Exception e) {
717 logger.trace("Shim initalization exception {}", config.port);
718 logger.trace("Shim initalization exception", e);
721 } catch (Exception e) {
722 logger.trace("Shim initalization exception {}", config.port);
723 logger.trace("Shim initalization exception", e);
730 private void scheduleConnectRetry(long waitSeconds) {
731 logger.trace("{} - Scheduling GoogleTV connection retry in {} seconds", handler.getThingID(), waitSeconds);
732 connectRetryJob = scheduler.schedule(this::connect, waitSeconds, TimeUnit.SECONDS);
736 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and
739 * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
740 * connect or reconnect, and true when calling from dispose.
742 private void disconnect(boolean interruptAll) {
743 synchronized (connectionLock) {
744 logger.debug("{} - Disconnecting GoogleTV", handler.getThingID());
746 this.isLoggedIn = false;
748 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
749 if (connectRetryJob != null) {
750 connectRetryJob.cancel(true);
752 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
753 if (keepAliveJob != null) {
754 keepAliveJob.cancel(true);
756 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
758 Thread senderThread = this.senderThread;
759 if (senderThread != null && senderThread.isAlive()) {
760 senderThread.interrupt();
763 Thread readerThread = this.readerThread;
764 if (readerThread != null && readerThread.isAlive()) {
765 readerThread.interrupt();
768 Thread shimSenderThread = this.shimSenderThread;
769 if (shimSenderThread != null && shimSenderThread.isAlive()) {
770 shimSenderThread.interrupt();
773 Thread shimReaderThread = this.shimReaderThread;
774 if (shimReaderThread != null && shimReaderThread.isAlive()) {
775 shimReaderThread.interrupt();
778 SSLSocket sslSocket = this.sslSocket;
779 if (sslSocket != null) {
782 } catch (IOException e) {
783 logger.debug("Error closing GoogleTV SSL socket: {}", e.getMessage());
785 this.sslSocket = null;
787 BufferedReader reader = this.reader;
788 if (reader != null) {
791 } catch (IOException e) {
792 logger.debug("Error closing reader: {}", e.getMessage());
795 BufferedWriter writer = this.writer;
796 if (writer != null) {
799 } catch (IOException e) {
800 logger.debug("Error closing writer: {}", e.getMessage());
804 Socket shimServerSocket = this.shimServerSocket;
805 if (shimServerSocket != null) {
807 shimServerSocket.close();
808 } catch (IOException e) {
809 logger.debug("Error closing GoogleTV SSL socket: {}", e.getMessage());
811 this.shimServerSocket = null;
813 BufferedReader shimReader = this.shimReader;
814 if (shimReader != null) {
817 } catch (IOException e) {
818 logger.debug("Error closing shimReader: {}", e.getMessage());
821 BufferedWriter shimWriter = this.shimWriter;
822 if (shimWriter != null) {
825 } catch (IOException e) {
826 logger.debug("Error closing shimWriter: {}", e.getMessage());
832 private void reconnect() {
833 synchronized (connectionLock) {
834 if (!this.disposing) {
835 logger.debug("{} - Attempting to reconnect to the GoogleTV", handler.getThingID());
836 setStatus(false, "offline.reconnecting");
844 * Method executed by the message sender thread (senderThread)
846 private void senderThreadJob() {
847 logger.debug("{} - Command sender thread started {}", handler.getThingID(), config.port);
849 while (!Thread.currentThread().isInterrupted() && writer != null) {
850 GoogleTVCommand command = sendQueue.take();
853 BufferedWriter writer = this.writer;
854 if (writer != null) {
855 logger.trace("{} - Raw GoogleTV command decodes as: {}", handler.getThingID(),
856 GoogleTVRequest.decodeMessage(command.toString()));
857 writer.write(command.toString());
860 } catch (InterruptedIOException e) {
861 logger.debug("Interrupted while sending to GoogleTV");
862 setStatus(false, "offline.interrupted");
863 break; // exit loop and terminate thread
864 } catch (IOException e) {
865 logger.warn("{} - Communication error, will try to reconnect GoogleTV. Error: {}",
866 handler.getThingID(), e.getMessage());
867 setStatus(false, "offline.communication-error-will-try-to-reconnect");
868 sendQueue.add(command); // Requeue command
869 this.isLoggedIn = false;
871 break; // reconnect() will start a new thread; terminate this one
873 if (config.delay > 0) {
874 Thread.sleep(config.delay); // introduce delay to throttle send rate
877 } catch (InterruptedException e) {
878 Thread.currentThread().interrupt();
880 logger.debug("{} - Command sender thread exiting {}", handler.getThingID(), config.port);
884 private void shimSenderThreadJob() {
885 logger.debug("Shim sender thread started");
887 while (!Thread.currentThread().isInterrupted() && shimWriter != null) {
888 GoogleTVCommand command = shimQueue.take();
891 BufferedWriter writer = this.shimWriter;
892 if (writer != null) {
893 logger.trace("Shim received from google: {}",
894 GoogleTVRequest.decodeMessage(command.toString()));
895 writer.write(command.toString());
898 } catch (InterruptedIOException e) {
899 logger.debug("Shim interrupted while sending.");
900 break; // exit loop and terminate thread
901 } catch (IOException e) {
902 logger.warn("Shim communication error. Error: {}", e.getMessage());
903 break; // reconnect() will start a new thread; terminate this one
906 } catch (InterruptedException e) {
907 Thread.currentThread().interrupt();
909 logger.debug("Command sender thread exiting");
914 * Method executed by the message reader thread (readerThread)
916 private void readerThreadJob() {
917 logger.debug("{} - Message reader thread started {}", handler.getThingID(), config.port);
919 BufferedReader reader = this.reader;
922 while (!Thread.interrupted() && reader != null) {
923 thisMsg = GoogleTVRequest.fixMessage(Integer.toHexString(reader.read()));
924 if (HARD_DROP.equals(thisMsg)) {
925 // Google has crashed the connection. Disconnect hard.
926 logger.debug("{} - readerThreadJob received ffffffff. Disconnecting hard.", handler.getThingID());
927 this.isLoggedIn = false;
932 length = Integer.parseInt(thisMsg.toString(), 16);
933 logger.trace("{} - readerThreadJob message length {}", handler.getThingID(), length);
935 sbReader = new StringBuffer();
936 sbReader.append(thisMsg.toString());
938 sbReader.append(thisMsg.toString());
942 if ((length > 0) && (current == length)) {
943 logger.trace("{} - GoogleTV Message: {} {}", handler.getThingID(), length, sbReader.toString());
944 messageParser.handleMessage(sbReader.toString());
946 String thisCommand = interceptMessages(sbReader.toString());
947 shimQueue.add(new GoogleTVCommand(GoogleTVRequest.encodeMessage(thisCommand)));
952 } catch (InterruptedIOException e) {
953 logger.debug("Interrupted while reading");
954 setStatus(false, "offline.interrupted");
955 } catch (IOException e) {
956 String message = e.getMessage();
957 if ((message != null) && (message.contains("certificate_unknown")) && (!config.mode.equals(PIN_MODE))
959 setStatus(false, "offline.pin-process-incomplete");
960 logger.debug("{} - GoogleTV PIN Process Incomplete", handler.getThingID());
961 reconnectTaskCancel(true);
962 startChildConnectionManager(this.config.port + 1, PIN_MODE);
963 } else if ((message != null) && (message.contains("certificate_unknown")) && (config.shim)) {
964 logger.debug("Shim cert_unknown I/O error while reading from stream: {}", e.getMessage());
965 Socket shimServerSocket = this.shimServerSocket;
966 if (shimServerSocket != null) {
968 shimServerSocket.close();
969 } catch (IOException ex) {
970 logger.debug("Error closing GoogleTV SSL socket: {}", ex.getMessage());
972 this.shimServerSocket = null;
975 logger.debug("I/O error while reading from stream: {}", e.getMessage());
976 setStatus(false, "offline.io-error");
978 } catch (RuntimeException e) {
979 logger.warn("Runtime exception in reader thread", e);
980 setStatus(false, "offline.runtime-exception");
982 logger.debug("{} - Message reader thread exiting {}", handler.getThingID(), config.port);
986 private String interceptMessages(String message) {
987 if (message.startsWith("080210c801c202", 2)) {
988 // intercept PIN hash and replace with valid shim hash
989 int length = this.pinHash.length() / 2;
990 String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
991 String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
992 String reply = "080210c801c202" + len1 + "0a" + len2 + this.pinHash;
993 String replyLength = GoogleTVRequest.fixMessage(Integer.toHexString(reply.length() / 2));
994 String finalReply = replyLength + reply;
995 logger.trace("Message Intercepted: {}", message);
996 logger.trace("Message chagnged to: {}", finalReply);
998 } else if (message.startsWith("080210c801ca02", 2)) {
999 // intercept PIN hash and replace with valid shim hash
1000 int length = this.shimPinHash.length() / 2;
1001 String len1 = GoogleTVRequest.fixMessage(Integer.toHexString(length + 2));
1002 String len2 = GoogleTVRequest.fixMessage(Integer.toHexString(length));
1003 String reply = "080210c801ca02" + len1 + "0a" + len2 + this.shimPinHash;
1004 String replyLength = GoogleTVRequest.fixMessage(Integer.toHexString(reply.length() / 2));
1005 String finalReply = replyLength + reply;
1006 logger.trace("Message Intercepted: {}", message);
1007 logger.trace("Message chagnged to: {}", finalReply);
1010 // don't intercept message
1015 private void shimReaderThreadJob() {
1016 logger.debug("Shim reader thread started {}", config.port);
1018 BufferedReader reader = this.shimReader;
1019 String thisShimMsg = "";
1022 while (!Thread.interrupted() && reader != null) {
1023 thisShimMsg = GoogleTVRequest.fixMessage(Integer.toHexString(reader.read()));
1024 if (HARD_DROP.equals(thisShimMsg)) {
1025 // Google has crashed the connection. Disconnect hard.
1030 length = Integer.parseInt(thisShimMsg.toString(), 16);
1031 logger.trace("shimReaderThreadJob message length {}", length);
1033 sbShimReader = new StringBuffer();
1034 sbShimReader.append(thisShimMsg.toString());
1036 sbShimReader.append(thisShimMsg.toString());
1039 if ((length > 0) && (current == length)) {
1040 logger.trace("Shim GoogleTV Message: {} {}", length, sbShimReader.toString());
1041 String thisCommand = interceptMessages(sbShimReader.toString());
1042 sendQueue.add(new GoogleTVCommand(GoogleTVRequest.encodeMessage(thisCommand)));
1046 } catch (InterruptedIOException e) {
1047 logger.debug("Interrupted while reading");
1048 setStatus(false, "offline.interrupted");
1049 } catch (IOException e) {
1050 logger.debug("I/O error while reading from stream: {}", e.getMessage());
1051 setStatus(false, "offline.io-error");
1052 } catch (RuntimeException e) {
1053 logger.warn("Runtime exception in reader thread", e);
1054 setStatus(false, "offline.runtime-exception");
1056 logger.debug("Shim message reader thread exiting {}", config.port);
1060 public void sendKeepAlive(String request) {
1061 String keepalive = GoogleTVRequest.encodeMessage(GoogleTVRequest.keepAlive(request));
1062 logger.debug("{} - Sending GoogleTV keepalive - request {} - response {}", handler.getThingID(), request,
1063 GoogleTVRequest.decodeMessage(keepalive));
1064 sendCommand(new GoogleTVCommand(keepalive));
1065 reconnectTaskSchedule();
1069 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should
1071 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
1073 private void reconnectTaskSchedule() {
1074 synchronized (keepAliveReconnectLock) {
1075 logger.trace("{} - Scheduling Reconnect Job for {}", handler.getThingID(), KEEPALIVE_TIMEOUT_SECONDS);
1076 keepAliveReconnectJob = scheduler.schedule(this::keepAliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
1082 * Cancels the reconnect task keepAliveReconnectJob.
1084 private void reconnectTaskCancel(boolean interrupt) {
1085 synchronized (keepAliveReconnectLock) {
1086 ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
1087 if (keepAliveReconnectJob != null) {
1088 logger.trace("{} - Canceling GoogleTV scheduled reconnect job.", handler.getThingID());
1089 keepAliveReconnectJob.cancel(interrupt);
1090 this.keepAliveReconnectJob = null;
1096 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
1097 * validMessageReceived() which in turn calls reconnectTaskCancel().
1099 private void keepAliveTimeoutExpired() {
1100 logger.debug("{} - GoogleTV keepalive response timeout expired. Initiating reconnect.", handler.getThingID());
1104 public void validMessageReceived() {
1105 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
1108 public void finishPinProcess() {
1109 GoogleTVConnectionManager connectionManager = this.connectionManager;
1110 GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1111 if ((connectionManager != null) && (config.mode.equals(PIN_MODE)) && (!config.shim)) {
1113 connectionManager.finishPinProcess();
1114 } else if ((childConnectionManager != null) && (config.mode.equals(DEFAULT_MODE)) && (!config.shim)) {
1115 childConnectionManager.dispose();
1120 public void sendCommand(GoogleTVCommand command) {
1121 if ((!config.shim) && (!command.isEmpty())) {
1122 int length = command.toString().length();
1123 String hexLength = GoogleTVRequest.encodeMessage(GoogleTVRequest.fixMessage(Integer.toHexString(length)));
1124 String message = hexLength + command.toString();
1125 GoogleTVCommand lenCommand = new GoogleTVCommand(message);
1126 sendQueue.add(lenCommand);
1130 public void sendShim(GoogleTVCommand command) {
1131 if (!command.isEmpty()) {
1132 shimQueue.add(command);
1136 public void handleCommand(ChannelUID channelUID, Command command) {
1137 logger.debug("{} - Command received: {}", handler.getThingID(), channelUID.getId());
1139 if (CHANNEL_KEYPRESS.equals(channelUID.getId())) {
1140 if (command instanceof StringType) {
1141 if (command.toString().length() == 5) {
1142 // Account for KEY_(ASCII Character)
1143 String keyPress = "aa01071a0512031a01"
1144 + GoogleTVRequest.decodeMessage(new String("" + command.toString().charAt(4)));
1145 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(keyPress)));
1149 String message = "";
1151 String shortCommand = command.toString();
1152 if (command.toString().endsWith("_PRESS")) {
1154 shortCommand = "KEY_" + command.toString().split("_")[1];
1155 } else if (command.toString().endsWith("_RELEASE")) {
1157 shortCommand = "KEY_" + command.toString().split("_")[1];
1162 switch (shortCommand) {
1164 message = "52040813" + suffix;
1167 message = "52040814" + suffix;
1170 message = "52040816" + suffix;
1173 message = "52040815" + suffix;
1176 message = "52040817" + suffix;
1179 message = "52040803" + suffix;
1182 message = "52040804" + suffix;
1185 message = "52040852" + suffix;
1188 message = "5204087E" + suffix;
1191 message = "5204087F" + suffix;
1193 case "KEY_PLAYPAUSE":
1194 message = "52040855" + suffix;
1197 message = "52040856" + suffix;
1200 message = "52040857" + suffix;
1202 case "KEY_PREVIOUS":
1203 message = "52040858" + suffix;
1206 message = "52040859" + suffix;
1209 message = "5204085A" + suffix;
1212 message = "5204081a" + suffix;
1215 message = "52040818" + suffix;
1218 message = "52040819" + suffix;
1221 message = "5204085b" + suffix;
1224 logger.debug("Unknown Key {}", command);
1227 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1229 } else if (CHANNEL_KEYCODE.equals(channelUID.getId())) {
1230 if (command instanceof StringType) {
1231 String shortCommand = command.toString().split("_")[0];
1232 int commandInt = Integer.parseInt(shortCommand, 10);
1234 if (commandInt > 255) {
1237 } else if (commandInt > 127) {
1241 String key = Integer.toHexString(commandInt) + suffix;
1243 if ((key.length() % 2) == 1) {
1249 if (command.toString().endsWith("_PRESS")) {
1251 } else if (command.toString().endsWith("_RELEASE")) {
1257 String length = "0" + (key.length() / 2);
1258 String message = "52" + length + key;
1260 logger.trace("Sending KEYCODE {} as {}", key, message);
1261 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1264 } else if (CHANNEL_PINCODE.equals(channelUID.getId())) {
1265 if (command instanceof StringType) {
1267 Certificate[] shimClientChain = this.shimClientChain;
1268 Certificate[] shimServerChain = this.shimServerChain;
1269 Certificate[] shimClientLocalChain = this.shimClientLocalChain;
1270 if (config.mode.equals(DEFAULT_MODE)) {
1271 if ((!isLoggedIn) && (command.toString().equals("REQUEST"))
1272 && (childConnectionManager == null)) {
1273 setStatus(false, "offline.user-forced-pin-process");
1274 logger.debug("{} - User Forced PIN Process", handler.getThingID());
1276 startChildConnectionManager(config.port + 1, PIN_MODE);
1278 Thread.sleep(PIN_DELAY);
1279 } catch (InterruptedException e) {
1280 logger.trace("InterruptedException", e);
1283 GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1284 if (childConnectionManager != null) {
1285 childConnectionManager.handleCommand(channelUID, command);
1287 logger.debug("{} - Child Connection Manager unavailable.", handler.getThingID());
1289 } else if ((config.mode.equals(PIN_MODE)) && (!config.shim)) {
1291 if (command.toString().equals("REQUEST")) {
1292 sendCommand(new GoogleTVCommand(
1293 GoogleTVRequest.encodeMessage(GoogleTVRequest.pinRequest(command.toString()))));
1294 } else if (shimServerChain != null) {
1295 this.pinHash = GoogleTVUtils.validatePIN(command.toString(), androidtvPKI.getCert(),
1296 shimServerChain[0]);
1297 sendCommand(new GoogleTVCommand(
1298 GoogleTVRequest.encodeMessage(GoogleTVRequest.pinRequest(this.pinHash))));
1301 } else if ((config.mode.equals(PIN_MODE)) && (config.shim)) {
1302 if ((shimClientChain != null) && (shimServerChain != null) && (shimClientLocalChain != null)) {
1303 this.pinHash = GoogleTVUtils.validatePIN(command.toString(), androidtvPKI.getCert(),
1304 shimServerChain[0]);
1305 this.shimPinHash = GoogleTVUtils.validatePIN(command.toString(), shimClientChain[0],
1306 shimClientLocalChain[0]);
1309 } catch (CertificateException e) {
1310 logger.trace("PIN CertificateException", e);
1313 } else if (CHANNEL_POWER.equals(channelUID.getId())) {
1314 if (command instanceof OnOffType) {
1315 if ((power && command.equals(OnOffType.OFF)) || (!power && command.equals(OnOffType.ON))) {
1316 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204081a1003")));
1318 } else if (command instanceof StringType) {
1319 if ((power && command.toString().equals("OFF")) || (!power && command.toString().equals("ON"))) {
1320 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204081a1003")));
1323 } else if (CHANNEL_MUTE.equals(channelUID.getId())) {
1324 if (command instanceof OnOffType) {
1325 if ((volMute && command.equals(OnOffType.OFF)) || (!volMute && command.equals(OnOffType.ON))) {
1326 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage("5204085b1003")));
1329 } else if (CHANNEL_DEBUG.equals(channelUID.getId())) {
1330 if (command instanceof StringType) {
1331 if (command.toString().startsWith("RAW", 9)) {
1332 String newCommand = command.toString().substring(13);
1333 String message = GoogleTVRequest.encodeMessage(newCommand);
1334 if (logger.isTraceEnabled()) {
1335 logger.trace("Raw Message Decodes as: {}", GoogleTVRequest.decodeMessage(message));
1337 sendCommand(new GoogleTVCommand(message));
1338 } else if (command.toString().startsWith("MSG", 9)) {
1339 String newCommand = command.toString().substring(13);
1340 messageParser.handleMessage(newCommand);
1343 } else if (CHANNEL_KEYBOARD.equals(channelUID.getId())) {
1344 if (command instanceof StringType) {
1345 String keyPress = "";
1346 for (int i = 0; i < command.toString().length(); i++) {
1347 keyPress = "aa01071a0512031a01"
1348 + GoogleTVRequest.decodeMessage(String.valueOf(command.toString().charAt(i)));
1349 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(keyPress)));
1352 } else if (CHANNEL_PLAYER.equals(channelUID.getId())) {
1353 String message = "";
1354 if (command == PlayPauseType.PAUSE || command == OnOffType.OFF) {
1355 message = "5204087F1003";
1356 } else if (command == PlayPauseType.PLAY || command == OnOffType.ON) {
1357 message = "5204087E1003";
1358 } else if (command == NextPreviousType.NEXT) {
1359 message = "520408571003";
1360 } else if (command == NextPreviousType.PREVIOUS) {
1361 message = "520408581003";
1362 } else if (command == RewindFastforwardType.FASTFORWARD) {
1363 message = "5204085A1003";
1364 } else if (command == RewindFastforwardType.REWIND) {
1365 message = "520408591003";
1367 sendCommand(new GoogleTVCommand(GoogleTVRequest.encodeMessage(message)));
1371 public void dispose() {
1372 this.disposing = true;
1374 Future<?> asyncInitializeTask = this.asyncInitializeTask;
1375 if (asyncInitializeTask != null) {
1376 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1378 Future<?> shimAsyncInitializeTask = this.shimAsyncInitializeTask;
1379 if (shimAsyncInitializeTask != null) {
1380 shimAsyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet
1382 ScheduledFuture<?> deviceHealthJob = this.deviceHealthJob;
1383 if (deviceHealthJob != null) {
1384 deviceHealthJob.cancel(true);
1386 GoogleTVConnectionManager childConnectionManager = this.childConnectionManager;
1387 if (childConnectionManager != null) {
1388 childConnectionManager.dispose();